inq 26.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 +7 -0
- data/.cirrus.yml +84 -0
- data/.codeclimate.yml +23 -0
- data/.github_changelog_generator +2 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +260 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +499 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +15 -0
- data/ISSUES.md +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +76 -0
- data/bin/console +14 -0
- data/bin/prerelease-generate-changelog +28 -0
- data/bin/setup +8 -0
- data/bors.toml +17 -0
- data/build-debug.rb +20 -0
- data/exe/inq +7 -0
- data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +597 -0
- data/fixtures/vcr_cassettes/how-is-example-repository.yml +768 -0
- data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +23940 -0
- data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +66 -0
- data/fixtures/vcr_cassettes/how-is-with-config-file.yml +23940 -0
- data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +81 -0
- data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +81 -0
- data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +402 -0
- data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +325 -0
- data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +325 -0
- data/inq.gemspec +45 -0
- data/lib/inq.rb +63 -0
- data/lib/inq/cacheable.rb +71 -0
- data/lib/inq/cli.rb +135 -0
- data/lib/inq/config.rb +123 -0
- data/lib/inq/constants.rb +9 -0
- data/lib/inq/date_time_helpers.rb +48 -0
- data/lib/inq/exe.rb +66 -0
- data/lib/inq/frontmatter.rb +51 -0
- data/lib/inq/report.rb +140 -0
- data/lib/inq/report_collection.rb +113 -0
- data/lib/inq/sources.rb +11 -0
- data/lib/inq/sources/ci/appveyor.rb +87 -0
- data/lib/inq/sources/ci/travis.rb +159 -0
- data/lib/inq/sources/github.rb +57 -0
- data/lib/inq/sources/github/contributions.rb +204 -0
- data/lib/inq/sources/github/issue_fetcher.rb +148 -0
- data/lib/inq/sources/github/issues.rb +126 -0
- data/lib/inq/sources/github/pulls.rb +29 -0
- data/lib/inq/sources/github_helpers.rb +106 -0
- data/lib/inq/template.rb +9 -0
- data/lib/inq/templates/contributions_partial.html +1 -0
- data/lib/inq/templates/issues_or_pulls_partial.html +7 -0
- data/lib/inq/templates/new_contributors_partial.html +5 -0
- data/lib/inq/templates/report.html +19 -0
- data/lib/inq/templates/report_partial.html +12 -0
- data/lib/inq/text.rb +26 -0
- data/lib/inq/version.rb +6 -0
- metadata +263 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "inq/version"
|
4
|
+
require "inq/date_time_helpers"
|
5
|
+
require "inq/sources/github"
|
6
|
+
require "inq/text"
|
7
|
+
|
8
|
+
module Inq
|
9
|
+
module Sources
|
10
|
+
class Github
|
11
|
+
##
|
12
|
+
# Fetches raw data for GitHub issues.
|
13
|
+
class IssueFetcher
|
14
|
+
include Inq::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] Inq::Issues or Inq::Pulls instance for which to fetch issues
|
47
|
+
def initialize(issues_source)
|
48
|
+
@issues_source = issues_source
|
49
|
+
@cache = issues_source.cache
|
50
|
+
@github = 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
|
+
Inq::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
|
+
Inq::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
|
+
Inq::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
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "inq/date_time_helpers"
|
4
|
+
require "inq/sources/github"
|
5
|
+
require "inq/sources/github_helpers"
|
6
|
+
require "inq/sources/github/issue_fetcher"
|
7
|
+
require "inq/template"
|
8
|
+
require "date"
|
9
|
+
|
10
|
+
module Inq
|
11
|
+
module Sources
|
12
|
+
class Github
|
13
|
+
##
|
14
|
+
# Fetches various information about GitHub Issues.
|
15
|
+
class Issues
|
16
|
+
include Inq::DateTimeHelpers
|
17
|
+
include Inq::Sources::GithubHelpers
|
18
|
+
|
19
|
+
attr_reader :config, :start_date, :end_date, :cache
|
20
|
+
|
21
|
+
# @param repository [String] GitHub repository name, of the format user/repo.
|
22
|
+
# @param start_date [String] Start date for the report being generated.
|
23
|
+
# @param end_date [String] End date for the report being generated.
|
24
|
+
# @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls
|
25
|
+
def initialize(config, start_date, end_date, cache)
|
26
|
+
@config = config
|
27
|
+
@cache = cache
|
28
|
+
@repository = config["repository"]
|
29
|
+
raise "#{self.class}.new() got nil repository." if @repository.nil?
|
30
|
+
@start_date = start_date
|
31
|
+
@end_date = end_date
|
32
|
+
end
|
33
|
+
|
34
|
+
def url(values = {})
|
35
|
+
defaults = {
|
36
|
+
"is" => singular_type,
|
37
|
+
"created" => "#{@start_date}..#{@end_date}",
|
38
|
+
}
|
39
|
+
values = defaults.merge(values)
|
40
|
+
raw_query = values.map { |k, v|
|
41
|
+
[k, v].join(":")
|
42
|
+
}.join(" ")
|
43
|
+
|
44
|
+
query = CGI.escape(raw_query)
|
45
|
+
|
46
|
+
"https://github.com/#{@repository}/#{url_suffix}?q=#{query}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def average_age
|
50
|
+
average_age_for(data)
|
51
|
+
end
|
52
|
+
|
53
|
+
def oldest
|
54
|
+
result = oldest_for(data)
|
55
|
+
return {} if result.nil?
|
56
|
+
|
57
|
+
result["date"] = pretty_date(result["createdAt"])
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def newest
|
63
|
+
result = newest_for(data)
|
64
|
+
return {} if result.nil?
|
65
|
+
|
66
|
+
result["date"] = pretty_date(result["createdAt"])
|
67
|
+
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def summary
|
72
|
+
number_open = to_a.length
|
73
|
+
pretty_number = pluralize(pretty_type, number_open, zero_is_no: false)
|
74
|
+
was_were = (number_open == 1) ? "was" : "were"
|
75
|
+
|
76
|
+
"<p>A total of <a href=\"#{url}\">#{pretty_number}</a> #{was_were} opened during this period.</p>"
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_html
|
80
|
+
return summary if to_a.empty?
|
81
|
+
|
82
|
+
Inq::Template.apply("issues_or_pulls_partial.html", {
|
83
|
+
summary: summary,
|
84
|
+
average_age: average_age,
|
85
|
+
pretty_type: pretty_type,
|
86
|
+
|
87
|
+
oldest_link: oldest["url"],
|
88
|
+
oldest_date: oldest["date"],
|
89
|
+
|
90
|
+
newest_link: newest["url"],
|
91
|
+
newest_date: newest["date"],
|
92
|
+
})
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_a
|
96
|
+
obj_to_array_of_hashes(data)
|
97
|
+
end
|
98
|
+
|
99
|
+
def type
|
100
|
+
singular_type + "s"
|
101
|
+
end
|
102
|
+
|
103
|
+
def pretty_type
|
104
|
+
"issue"
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def url_suffix
|
110
|
+
"issues"
|
111
|
+
end
|
112
|
+
|
113
|
+
def singular_type
|
114
|
+
"issue"
|
115
|
+
end
|
116
|
+
|
117
|
+
def data
|
118
|
+
return @data if instance_variable_defined?(:@data)
|
119
|
+
|
120
|
+
fetcher = IssueFetcher.new(self)
|
121
|
+
@data = fetcher.data
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "inq/sources/github/issues"
|
4
|
+
|
5
|
+
module Inq
|
6
|
+
module Sources
|
7
|
+
class Github
|
8
|
+
##
|
9
|
+
# Fetches various information about GitHub Pull Requests
|
10
|
+
class Pulls < Issues
|
11
|
+
def url_suffix
|
12
|
+
"pulls"
|
13
|
+
end
|
14
|
+
|
15
|
+
def singular_type
|
16
|
+
"pull"
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
"pullRequests"
|
21
|
+
end
|
22
|
+
|
23
|
+
def pretty_type
|
24
|
+
"pull request"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "inq/sources/github"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module Inq
|
7
|
+
module Sources
|
8
|
+
##
|
9
|
+
# Helper functions used by GitHub-related sources.
|
10
|
+
module GithubHelpers
|
11
|
+
def obj_to_array_of_hashes(object)
|
12
|
+
object.to_a.map(&:to_h)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Given an Array of issues or pulls, return the average age of them.
|
16
|
+
# Returns nil if no issues or pulls are provided.
|
17
|
+
def average_age_for(issues_or_pulls)
|
18
|
+
return nil if issues_or_pulls.empty?
|
19
|
+
|
20
|
+
ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["createdAt"]) }
|
21
|
+
average_age_in_seconds = ages.reduce(:+) / ages.length
|
22
|
+
|
23
|
+
values =
|
24
|
+
period_pairs_for(average_age_in_seconds) \
|
25
|
+
.reject { |(v, _)| v.zero? } \
|
26
|
+
.map { |(v, k)| pluralize(k, v) }
|
27
|
+
|
28
|
+
value = values[0, 2].join(" and ")
|
29
|
+
|
30
|
+
"approximately #{value}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def sort_iops_by_created_at(issues_or_pulls)
|
34
|
+
issues_or_pulls.sort_by { |x| DateTime.parse(x["createdAt"]) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Given an Array of issues or pulls, return the oldest.
|
38
|
+
# Returns nil if no issues or pulls are provided.
|
39
|
+
def oldest_for(issues_or_pulls)
|
40
|
+
return nil if issues_or_pulls.empty?
|
41
|
+
|
42
|
+
sort_iops_by_created_at(issues_or_pulls).first
|
43
|
+
end
|
44
|
+
|
45
|
+
# Given an Array of issues or pulls, return the newest.
|
46
|
+
# Returns nil if no issues or pulls are provided.
|
47
|
+
def newest_for(issues_or_pulls)
|
48
|
+
return nil if issues_or_pulls.empty?
|
49
|
+
|
50
|
+
sort_iops_by_created_at(issues_or_pulls).last
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Returns how many seconds ago a date (as a String) was.
|
56
|
+
def time_ago_in_seconds(x)
|
57
|
+
DateTime.now.strftime("%s").to_i - DateTime.parse(x).strftime("%s").to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
SECONDS_IN_A_YEAR = 31_556_926
|
61
|
+
SECONDS_IN_A_MONTH = 2_629_743
|
62
|
+
SECONDS_IN_A_WEEK = 604_800
|
63
|
+
SECONDS_IN_A_DAY = 86_400
|
64
|
+
|
65
|
+
# Calculates a list of pairs of value and period label.
|
66
|
+
#
|
67
|
+
# @param age_in_seconds [Float]
|
68
|
+
#
|
69
|
+
# @return [Array<Array>] The input age_in_seconds expressed as different
|
70
|
+
# units, as pairs of value and unit name.
|
71
|
+
def period_pairs_for(age_in_seconds)
|
72
|
+
years_remainder = age_in_seconds % SECONDS_IN_A_YEAR
|
73
|
+
|
74
|
+
months_remainder = years_remainder % SECONDS_IN_A_MONTH
|
75
|
+
|
76
|
+
weeks_remainder = months_remainder % SECONDS_IN_A_WEEK
|
77
|
+
|
78
|
+
[
|
79
|
+
[age_in_seconds / SECONDS_IN_A_YEAR, "year"],
|
80
|
+
[years_remainder / SECONDS_IN_A_MONTH, "month"],
|
81
|
+
[months_remainder / SECONDS_IN_A_WEEK, "week"],
|
82
|
+
[weeks_remainder / SECONDS_IN_A_DAY, "day"],
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
def pluralize(string, number, zero_is_no: false)
|
87
|
+
number_str = number
|
88
|
+
number_str = "no" if number.zero? && zero_is_no
|
89
|
+
|
90
|
+
"#{number_str} #{string}#{(number == 1) ? '' : 's'}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def pretty_date(date_or_str)
|
94
|
+
if date_or_str.is_a?(DateTime)
|
95
|
+
date = datetime_or_str
|
96
|
+
elsif date_or_str.is_a?(String)
|
97
|
+
date = DateTime.parse(date_or_str)
|
98
|
+
else
|
99
|
+
raise ArgumentError, "expected DateTime or String, got #{date_or_str.class}"
|
100
|
+
end
|
101
|
+
|
102
|
+
date.strftime("%b %d, %Y")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/inq/template.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
%{start_text}, %{user}/%{repo} gained <a href="%{compare_url}">%{new_commits}</a>, contributed by %{authors}. There %{additions_count_str} %{additions} and %{deletions} across %{changed_files}.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
%{frontmatter}<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>%{title}</title>
|
5
|
+
<style>
|
6
|
+
body { font: sans-serif; }
|
7
|
+
main {
|
8
|
+
max-width: 600px;
|
9
|
+
max-width: 72ch;
|
10
|
+
margin: auto;
|
11
|
+
}
|
12
|
+
</style>
|
13
|
+
</head>
|
14
|
+
<body>
|
15
|
+
<main>
|
16
|
+
%{report}
|
17
|
+
</main>
|
18
|
+
</body>
|
19
|
+
</html>
|