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
@@ -1,274 +1,125 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "how_is/
|
3
|
+
require "how_is/date_time_helpers"
|
4
4
|
require "how_is/sources/github"
|
5
5
|
require "how_is/sources/github_helpers"
|
6
|
+
require "how_is/sources/github/issue_fetcher"
|
7
|
+
require "how_is/template"
|
6
8
|
require "date"
|
7
9
|
|
8
|
-
module HowIs
|
9
|
-
|
10
|
-
class
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
query = CGI.escape(raw_query)
|
33
|
-
|
34
|
-
"https://github.com/#{@repository}/#{url_suffix}?q=#{query}"
|
35
|
-
end
|
36
|
-
|
37
|
-
def average_age
|
38
|
-
average_age_for(data)
|
39
|
-
end
|
40
|
-
|
41
|
-
def oldest
|
42
|
-
oldest_for(data) || {}
|
43
|
-
end
|
44
|
-
|
45
|
-
def newest
|
46
|
-
newest_for(data) || {}
|
47
|
-
end
|
48
|
-
|
49
|
-
def summary
|
50
|
-
number_open = to_a.length
|
51
|
-
pretty_number =
|
52
|
-
pluralize(pretty_type, number_open, zero_is_no: true)
|
10
|
+
module HowIs
|
11
|
+
module Sources
|
12
|
+
class Github
|
13
|
+
##
|
14
|
+
# Fetches various information about GitHub Issues.
|
15
|
+
class Issues
|
16
|
+
include HowIs::DateTimeHelpers
|
17
|
+
include HowIs::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 HowIs::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
|
53
33
|
|
54
|
-
|
55
|
-
|
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(" ")
|
56
43
|
|
57
|
-
|
58
|
-
summary_ = "<p>#{summary}</p>"
|
44
|
+
query = CGI.escape(raw_query)
|
59
45
|
|
60
|
-
|
46
|
+
"https://github.com/#{@repository}/#{url_suffix}?q=#{query}"
|
47
|
+
end
|
61
48
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
type: type,
|
66
|
-
pretty_type: pretty_type,
|
49
|
+
def average_age
|
50
|
+
average_age_for(data)
|
51
|
+
end
|
67
52
|
|
68
|
-
|
69
|
-
|
53
|
+
def oldest
|
54
|
+
result = oldest_for(data)
|
55
|
+
return {} if result.nil?
|
70
56
|
|
71
|
-
|
72
|
-
newest_date: pretty_date(newest["createdAt"]),
|
73
|
-
}
|
57
|
+
result["date"] = pretty_date(result["createdAt"])
|
74
58
|
|
75
|
-
|
76
|
-
|
59
|
+
result
|
60
|
+
end
|
77
61
|
|
78
|
-
|
62
|
+
def newest
|
63
|
+
result = newest_for(data)
|
64
|
+
return {} if result.nil?
|
79
65
|
|
80
|
-
|
81
|
-
ipl = with_label_links(num_with_label(data), @repository)
|
82
|
-
number_with_no_label = num_with_no_label(data)
|
66
|
+
result["date"] = pretty_date(result["createdAt"])
|
83
67
|
|
84
|
-
|
85
|
-
ipl["(No label)"] = {
|
86
|
-
"name" => "(No label)",
|
87
|
-
"total" => number_with_no_label,
|
88
|
-
}
|
68
|
+
result
|
89
69
|
end
|
90
70
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
<tr>
|
96
|
-
<td style="width: %{label_width}">%{label_text}</td>
|
97
|
-
<td><span class="fill" style="width: %{percentage}%%">%{link_text}</span></td>
|
98
|
-
</tr>
|
99
|
-
EOF
|
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"
|
100
75
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
return "<p>There are no open issues to graph.</p>" if ipl.empty?
|
76
|
+
"<p>A total of <a href=\"#{url}\">#{pretty_number}</a> #{was_were} opened during this period.</p>"
|
77
|
+
end
|
105
78
|
|
106
|
-
|
107
|
-
|
79
|
+
def to_html
|
80
|
+
return summary if to_a.empty?
|
108
81
|
|
109
|
-
|
110
|
-
|
82
|
+
HowIs::Template.apply("issues_or_pulls_partial.html", {
|
83
|
+
summary: summary,
|
84
|
+
average_age: average_age,
|
85
|
+
pretty_type: pretty_type,
|
111
86
|
|
112
|
-
|
113
|
-
|
114
|
-
label_text = label
|
115
|
-
label_url = label_url_for(info["name"])
|
116
|
-
label_text = '<a href="' + label_url + '">' + label_text + '</a>'
|
87
|
+
oldest_link: oldest["url"],
|
88
|
+
oldest_date: oldest["date"],
|
117
89
|
|
118
|
-
|
119
|
-
|
120
|
-
label_text: label_text,
|
121
|
-
label_link: info["url"],
|
122
|
-
percentage: get_percentage.call(info["total"]),
|
123
|
-
link_text: info["total"].to_s,
|
90
|
+
newest_link: newest["url"],
|
91
|
+
newest_date: newest["date"],
|
124
92
|
})
|
125
|
-
}
|
126
|
-
|
127
|
-
"<table class=\"horizontal-bar-graph\">\n" +
|
128
|
-
parts.join("\n") +
|
129
|
-
"\n</table>"
|
130
|
-
end
|
131
|
-
|
132
|
-
def to_a
|
133
|
-
obj_to_array_of_hashes(data)
|
134
|
-
end
|
135
|
-
|
136
|
-
private
|
137
|
-
|
138
|
-
def url_suffix
|
139
|
-
"issues"
|
140
|
-
end
|
141
|
-
|
142
|
-
def singular_type
|
143
|
-
"issue"
|
144
|
-
end
|
145
|
-
|
146
|
-
def type
|
147
|
-
singular_type + "s"
|
148
|
-
end
|
149
|
-
|
150
|
-
def pretty_type
|
151
|
-
"issue"
|
152
|
-
end
|
153
|
-
|
154
|
-
def data
|
155
|
-
return @data if instance_variable_defined?(:@data)
|
156
|
-
|
157
|
-
@data = []
|
158
|
-
return @data if last_cursor.nil?
|
159
|
-
|
160
|
-
after = nil
|
161
|
-
data = []
|
162
|
-
until after == TERMINATE_GRAPHQL_LOOP
|
163
|
-
after, data = fetch_issues(after, data)
|
164
93
|
end
|
165
94
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
def issue_is_relevant?(issue)
|
170
|
-
if !issue["closedAt"].nil? && date_le(issue["closedAt"], @start_date)
|
171
|
-
false
|
172
|
-
else
|
173
|
-
date_ge(issue["createdAt"], @start_date) && date_le(issue["createdAt"], @end_date)
|
95
|
+
def to_a
|
96
|
+
obj_to_array_of_hashes(data)
|
174
97
|
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def graphql(query_string)
|
178
|
-
query = Okay::GraphQL.query(query_string)
|
179
|
-
headers = {bearer_token: HowIs::Sources::Github::ACCESS_TOKEN}
|
180
|
-
query.submit!(:github, headers).or_raise!.from_json
|
181
|
-
end
|
182
98
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
raw_data = graphql <<~QUERY
|
187
|
-
repository(owner: #{@user.inspect}, name: #{@repo.inspect}) {
|
188
|
-
#{type}(last: 1, orderBy:{field: CREATED_AT, direction: ASC}) {
|
189
|
-
edges {
|
190
|
-
cursor
|
191
|
-
}
|
192
|
-
}
|
193
|
-
}
|
194
|
-
QUERY
|
195
|
-
|
196
|
-
edges = raw_data.dig("data", "repository", type, "edges")
|
197
|
-
@last_cursor =
|
198
|
-
if edges.nil? || edges.empty?
|
199
|
-
nil
|
200
|
-
else
|
201
|
-
edges.last["cursor"]
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def fetch_issues(after, data)
|
206
|
-
data ||= []
|
207
|
-
chunk_size = 100
|
208
|
-
after_str = ", after: #{after.inspect}" unless after.nil?
|
209
|
-
|
210
|
-
raw_data = graphql <<~QUERY
|
211
|
-
repository(owner: #{@user.inspect}, name: #{@repo.inspect}) {
|
212
|
-
#{type}(first: #{chunk_size}#{after_str}, orderBy:{field: CREATED_AT, direction: ASC}) {
|
213
|
-
edges {
|
214
|
-
cursor
|
215
|
-
node {
|
216
|
-
number
|
217
|
-
createdAt
|
218
|
-
closedAt
|
219
|
-
updatedAt
|
220
|
-
state
|
221
|
-
title
|
222
|
-
url
|
223
|
-
labels(first: 100) {
|
224
|
-
nodes {
|
225
|
-
name
|
226
|
-
}
|
227
|
-
}
|
228
|
-
}
|
229
|
-
}
|
230
|
-
}
|
231
|
-
}
|
232
|
-
QUERY
|
233
|
-
|
234
|
-
edges = raw_data.dig("data", "repository", type, "edges")
|
235
|
-
|
236
|
-
current_last_cursor = edges.last["cursor"]
|
237
|
-
|
238
|
-
unless edges.nil?
|
239
|
-
new_data = edges.map { |issue|
|
240
|
-
node = issue["node"]
|
241
|
-
node["labels"] = node["labels"]["nodes"]
|
242
|
-
|
243
|
-
node
|
244
|
-
}
|
245
|
-
|
246
|
-
data += new_data
|
99
|
+
def type
|
100
|
+
singular_type + "s"
|
247
101
|
end
|
248
102
|
|
249
|
-
|
250
|
-
|
103
|
+
def pretty_type
|
104
|
+
"issue"
|
251
105
|
end
|
252
106
|
|
253
|
-
|
254
|
-
end
|
255
|
-
|
256
|
-
def date_le(left, right)
|
257
|
-
left = str_to_dt(left)
|
258
|
-
right = str_to_dt(right)
|
107
|
+
private
|
259
108
|
|
260
|
-
|
261
|
-
|
109
|
+
def url_suffix
|
110
|
+
"issues"
|
111
|
+
end
|
262
112
|
|
263
|
-
|
264
|
-
|
265
|
-
|
113
|
+
def singular_type
|
114
|
+
"issue"
|
115
|
+
end
|
266
116
|
|
267
|
-
|
268
|
-
|
117
|
+
def data
|
118
|
+
return @data if instance_variable_defined?(:@data)
|
269
119
|
|
270
|
-
|
271
|
-
|
120
|
+
fetcher = IssueFetcher.new(self)
|
121
|
+
@data = fetcher.data
|
122
|
+
end
|
272
123
|
end
|
273
124
|
end
|
274
125
|
end
|
@@ -1,27 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "how_is/
|
4
|
-
require "how_is/sources/github"
|
5
|
-
require "how_is/sources/github_helpers"
|
6
|
-
require "date"
|
3
|
+
require "how_is/sources/github/issues"
|
7
4
|
|
8
|
-
module HowIs
|
9
|
-
|
10
|
-
class
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
module HowIs
|
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
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def singular_type
|
16
|
+
"pull"
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def type
|
20
|
+
"pullRequests"
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
def pretty_type
|
24
|
+
"pull request"
|
25
|
+
end
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -2,34 +2,56 @@
|
|
2
2
|
|
3
3
|
require "how_is/version"
|
4
4
|
require "how_is/sources"
|
5
|
-
require "github_api"
|
6
5
|
require "okay/graphql"
|
7
6
|
|
8
7
|
module HowIs
|
9
8
|
module Sources
|
10
9
|
# Contains configuration information for GitHub-based sources.
|
11
10
|
class Github
|
12
|
-
|
13
|
-
# is undefined.
|
14
|
-
class ConfigurationError < StandardError
|
15
|
-
def initialize(env_variable)
|
16
|
-
super("environment variable #{env_variable} not defined." \
|
17
|
-
" See README.md for details.")
|
18
|
-
end
|
11
|
+
class ConfigError < StandardError
|
19
12
|
end
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
raise ConfigurationError, "HOWIS_GITHUB_TOKEN" if ACCESS_TOKEN.nil?
|
14
|
+
def initialize(config)
|
15
|
+
must_have_key!(config, "sources/github")
|
24
16
|
|
25
|
-
|
26
|
-
BASIC_AUTH = ENV["HOWIS_BASIC_AUTH"]
|
27
|
-
raise ConfigurationError, "HOWIS_BASIC_AUTH" if BASIC_AUTH.nil?
|
17
|
+
@config = config["sources/github"]
|
28
18
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
19
|
+
must_have_key!(@config, "username")
|
20
|
+
must_have_key!(@config, "token")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Verify that +hash+ has a particular +key+.
|
24
|
+
#
|
25
|
+
# @raise [ConfigError] If +hash+ does not have the required +key+.
|
26
|
+
def must_have_key!(hash, key)
|
27
|
+
raise ConfigError, "Expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
28
|
+
raise ConfigError, "Expected key `#{key}'" unless hash.has_key?(key)
|
29
|
+
end
|
30
|
+
private :must_have_key!
|
31
|
+
|
32
|
+
# The GitHub username used for authenticating with GitHub.
|
33
|
+
def username
|
34
|
+
@config["username"]
|
35
|
+
end
|
36
|
+
|
37
|
+
# A GitHub Personal Access Token which goes with +username+.
|
38
|
+
def access_token
|
39
|
+
@config["token"]
|
40
|
+
end
|
41
|
+
|
42
|
+
# A string containing both the GitHub username and access token,
|
43
|
+
# used in instances where we use Basic Auth.
|
44
|
+
def basic_auth
|
45
|
+
"#{username}:#{access_token}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Submit a GraphQL query, and convert it from JSON to a Ruby object.
|
49
|
+
def graphql(query_string)
|
50
|
+
Okay::GraphQL.query(query_string)
|
51
|
+
.submit!(:github, {bearer_token: access_token})
|
52
|
+
.or_raise!
|
53
|
+
.from_json
|
54
|
+
end
|
33
55
|
end
|
34
56
|
end
|
35
57
|
end
|
@@ -5,46 +5,13 @@ require "date"
|
|
5
5
|
|
6
6
|
module HowIs
|
7
7
|
module Sources
|
8
|
+
##
|
9
|
+
# Helper functions used by GitHub-related sources.
|
8
10
|
module GithubHelpers
|
9
11
|
def obj_to_array_of_hashes(object)
|
10
12
|
object.to_a.map(&:to_h)
|
11
13
|
end
|
12
14
|
|
13
|
-
# Given an Array of issues or pulls, return a Hash specifying how many
|
14
|
-
# issues or pulls use each label.
|
15
|
-
def num_with_label(issues_or_pulls)
|
16
|
-
# Returned hash maps labels to frequency.
|
17
|
-
# E.g., given 10 issues/pulls with label "label1" and 5 with label "label2",
|
18
|
-
# {
|
19
|
-
# "label1" => 10,
|
20
|
-
# "label2" => 5
|
21
|
-
# }
|
22
|
-
|
23
|
-
hash = Hash.new(0)
|
24
|
-
issues_or_pulls.each do |iop|
|
25
|
-
next unless iop["labels"]
|
26
|
-
|
27
|
-
iop["labels"].each do |label|
|
28
|
-
hash[label["name"]] += 1
|
29
|
-
end
|
30
|
-
end
|
31
|
-
hash
|
32
|
-
end
|
33
|
-
|
34
|
-
# Returns the number of issues with no label.
|
35
|
-
def num_with_no_label(issues)
|
36
|
-
issues.select { |x| x["labels"].empty? }.length
|
37
|
-
end
|
38
|
-
|
39
|
-
# Given an Array of dates, average the timestamps and return the date that
|
40
|
-
# represents.
|
41
|
-
def average_date_for(issues_or_pulls)
|
42
|
-
timestamps = issues_or_pulls.map { |iop| DateTime.parse(iop["createdAt"]).strftime("%s").to_i }
|
43
|
-
average_timestamp = timestamps.reduce(:+) / issues_or_pulls.length
|
44
|
-
|
45
|
-
DateTime.strptime(average_timestamp.to_s, "%s")
|
46
|
-
end
|
47
|
-
|
48
15
|
# Given an Array of issues or pulls, return the average age of them.
|
49
16
|
# Returns nil if no issues or pulls are provided.
|
50
17
|
def average_age_for(issues_or_pulls)
|
@@ -53,19 +20,12 @@ module HowIs
|
|
53
20
|
ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["createdAt"]) }
|
54
21
|
average_age_in_seconds = ages.reduce(:+) / ages.length
|
55
22
|
|
56
|
-
values =
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
most_significant = values[0, 2].map { |x| x.join(" ") }
|
23
|
+
values =
|
24
|
+
period_pairs_for(average_age_in_seconds) \
|
25
|
+
.reject { |(v, _)| v.zero? } \
|
26
|
+
.map { |(v, k)| pluralize(k, v) }
|
62
27
|
|
63
|
-
value =
|
64
|
-
if most_significant.length < 2
|
65
|
-
most_significant.first
|
66
|
-
else
|
67
|
-
most_significant.join(" and ")
|
68
|
-
end
|
28
|
+
value = values[0, 2].join(" and ")
|
69
29
|
|
70
30
|
"approximately #{value}"
|
71
31
|
end
|
@@ -90,48 +50,13 @@ module HowIs
|
|
90
50
|
sort_iops_by_created_at(issues_or_pulls).last
|
91
51
|
end
|
92
52
|
|
93
|
-
# Given an issue or PR, returns the date it was created.
|
94
|
-
def date_for(issue_or_pull)
|
95
|
-
DateTime.parse(issue_or_pull["createdAt"])
|
96
|
-
end
|
97
|
-
|
98
|
-
def label_url_for(label_name)
|
99
|
-
if label_name == "(No label)"
|
100
|
-
url({"no"=>"label"})
|
101
|
-
else
|
102
|
-
url({"label"=>label_name})
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
53
|
private
|
107
54
|
|
108
|
-
# Takes an Array of labels, and returns amodified list that includes links
|
109
|
-
# to each label.
|
110
|
-
def with_label_links(labels, repository)
|
111
|
-
labels.map { |label, num_issues|
|
112
|
-
label_link = "https://github.com/#{repository}/issues?q=" + CGI.escape("is:open is:issue label:\"#{label}\"")
|
113
|
-
|
114
|
-
[label, {"link" => label_link, "total" => num_issues}]
|
115
|
-
}.to_h
|
116
|
-
end
|
117
|
-
|
118
55
|
# Returns how many seconds ago a date (as a String) was.
|
119
56
|
def time_ago_in_seconds(x)
|
120
57
|
DateTime.now.strftime("%s").to_i - DateTime.parse(x).strftime("%s").to_i
|
121
58
|
end
|
122
59
|
|
123
|
-
def issue_or_pull_to_hash(iop)
|
124
|
-
return nil if iop.nil?
|
125
|
-
|
126
|
-
ret = {}
|
127
|
-
|
128
|
-
ret["html_url"] = iop["html_url"]
|
129
|
-
ret["number"] = iop["number"]
|
130
|
-
ret["date"] = date_for(iop)
|
131
|
-
|
132
|
-
ret
|
133
|
-
end
|
134
|
-
|
135
60
|
SECONDS_IN_A_YEAR = 31_556_926
|
136
61
|
SECONDS_IN_A_MONTH = 2_629_743
|
137
62
|
SECONDS_IN_A_WEEK = 604_800
|
@@ -165,14 +90,6 @@ module HowIs
|
|
165
90
|
"#{number_str} #{string}#{(number == 1) ? '' : 's'}"
|
166
91
|
end
|
167
92
|
|
168
|
-
def are_or_is(number)
|
169
|
-
if number == 1
|
170
|
-
"is"
|
171
|
-
else
|
172
|
-
"are"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
93
|
def pretty_date(date_or_str)
|
177
94
|
if date_or_str.is_a?(DateTime)
|
178
95
|
date = datetime_or_str
|
@@ -182,7 +99,7 @@ module HowIs
|
|
182
99
|
raise ArgumentError, "expected DateTime or String, got #{date_or_str.class}"
|
183
100
|
end
|
184
101
|
|
185
|
-
date.strftime("%b %
|
102
|
+
date.strftime("%b %d, %Y")
|
186
103
|
end
|
187
104
|
end
|
188
105
|
end
|
data/lib/how_is/sources.rb
CHANGED
@@ -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}.
|
data/lib/how_is/templates/{issues_or_pulls_partial.html_template → issues_or_pulls_partial.html}
RENAMED
File without changes
|
@@ -9,14 +9,6 @@
|
|
9
9
|
max-width: 72ch;
|
10
10
|
margin: auto;
|
11
11
|
}
|
12
|
-
.horizontal-bar-graph {
|
13
|
-
position: relative;
|
14
|
-
width: 100%%; /* lol Kernel.format() disapproves. */
|
15
|
-
}
|
16
|
-
.horizontal-bar-graph .fill {
|
17
|
-
display: inline-block;
|
18
|
-
background: #CCC;
|
19
|
-
}
|
20
12
|
</style>
|
21
13
|
</head>
|
22
14
|
<body>
|