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
@@ -1,274 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "how_is/version"
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::Sources
9
- class Github
10
- class Issues
11
- include HowIs::Sources::GithubHelpers
12
-
13
- TERMINATE_GRAPHQL_LOOP = :terminate_graphql_loop
14
-
15
- def initialize(repository, start_date, end_date)
16
- @repository = repository
17
- @user, @repo = repository.split("/", 2)
18
- @start_date = start_date
19
- @end_date = end_date
20
- end
21
-
22
- def url(values = {})
23
- defaults = {
24
- "is" => singular_type,
25
- "created" => "#{@start_date}..#{@end_date}",
26
- }
27
- values = defaults.merge(values)
28
- raw_query = values.map { |k, v|
29
- [k, v].join(":")
30
- }.join(" ")
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
- "There #{are_or_is(number_open)} <a href=\"#{url}\">#{pretty_number} open</a>."
55
- end
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
- def to_html
58
- summary_ = "<p>#{summary}</p>"
44
+ query = CGI.escape(raw_query)
59
45
 
60
- return summary_ if to_a.empty?
46
+ "https://github.com/#{@repository}/#{url_suffix}?q=#{query}"
47
+ end
61
48
 
62
- template_data = {
63
- summary: summary_,
64
- average_age: average_age,
65
- type: type,
66
- pretty_type: pretty_type,
49
+ def average_age
50
+ average_age_for(data)
51
+ end
67
52
 
68
- oldest_link: oldest["url"],
69
- oldest_date: pretty_date(oldest["createdAt"]),
53
+ def oldest
54
+ result = oldest_for(data)
55
+ return {} if result.nil?
70
56
 
71
- newest_link: newest["url"],
72
- newest_date: pretty_date(newest["createdAt"]),
73
- }
57
+ result["date"] = pretty_date(result["createdAt"])
74
58
 
75
- Kernel.format(HowIs.template("issues_or_pulls_partial.html_template"), template_data)
76
- end
59
+ result
60
+ end
77
61
 
78
- # TODO: Clean up Issues Per Label stuff, or replace it with different functionality.
62
+ def newest
63
+ result = newest_for(data)
64
+ return {} if result.nil?
79
65
 
80
- def issues_per_label
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
- if number_with_no_label > 0
85
- ipl["(No label)"] = {
86
- "name" => "(No label)",
87
- "total" => number_with_no_label,
88
- }
68
+ result
89
69
  end
90
70
 
91
- ipl
92
- end
93
-
94
- HTML_GRAPH_ROW = <<-EOF
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
- def issues_per_label_html
102
- ipl = issues_per_label
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
- biggest = ipl.map { |_label, info| info["total"] }.max
107
- get_percentage = ->(number_of_issues) { number_of_issues * 100 / biggest }
79
+ def to_html
80
+ return summary if to_a.empty?
108
81
 
109
- longest_label_length = ipl.map(&:first).map(&:length).max
110
- label_width = "#{longest_label_length}ch"
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
- parts = ipl.map { |label, info|
113
- # TODO: Remove this hack to get around unlabeled issues not having a link.
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
- Kernel.format(HTML_GRAPH_ROW, {
119
- label_width: label_width,
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
- @data = data.select(&method(:issue_is_relevant?))
167
- end
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
- def last_cursor
184
- return @last_cursor if instance_variable_defined?(:@last_cursor)
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
- if current_last_cursor == last_cursor
250
- current_last_cursor = TERMINATE_GRAPHQL_LOOP
103
+ def pretty_type
104
+ "issue"
251
105
  end
252
106
 
253
- [current_last_cursor, data]
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
- left <= right
261
- end
109
+ def url_suffix
110
+ "issues"
111
+ end
262
112
 
263
- def date_ge(left, right)
264
- left = str_to_dt(left)
265
- right = str_to_dt(right)
113
+ def singular_type
114
+ "issue"
115
+ end
266
116
 
267
- left >= right
268
- end
117
+ def data
118
+ return @data if instance_variable_defined?(:@data)
269
119
 
270
- def str_to_dt(str)
271
- DateTime.parse(str)
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/version"
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::Sources
9
- class Github
10
- class Pulls < Issues
11
- def url_suffix
12
- "pulls"
13
- end
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
- def singular_type
16
- "pull"
17
- end
15
+ def singular_type
16
+ "pull"
17
+ end
18
18
 
19
- def type
20
- "pullRequests"
21
- end
19
+ def type
20
+ "pullRequests"
21
+ end
22
22
 
23
- def pretty_type
24
- "pull request"
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
- # An exception which is only raised if an environment variable
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
- # A GitHub Personal Access Token.
22
- ACCESS_TOKEN = ENV["HOWIS_GITHUB_TOKEN"]
23
- raise ConfigurationError, "HOWIS_GITHUB_TOKEN" if ACCESS_TOKEN.nil?
14
+ def initialize(config)
15
+ must_have_key!(config, "sources/github")
24
16
 
25
- # "<github username>:<personal access token>"
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
- # Used for the the Authorization header when talking to the
30
- # GitHub API.
31
- # https://developer.github.com/v4/guides/forming-calls/#communicating-with-graphql
32
- AUTHORIZATION_HEADER = "bearer " + ACCESS_TOKEN
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 = period_pairs_for(average_age_in_seconds).reject { |(v, _)| v.zero? }.map { |(v, k)|
57
- k += "s" if v != 1
58
- [v, k]
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 %_d, %Y")
102
+ date.strftime("%b %d, %Y")
186
103
  end
187
104
  end
188
105
  end
@@ -3,6 +3,8 @@
3
3
  require "how_is/version"
4
4
 
5
5
  module HowIs
6
+ ##
7
+ # Various information sources used by HowIs.
6
8
  module Sources
7
9
  # Simply for creating a namespace.
8
10
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "how_is/version"
4
+ require "okay/template"
5
+
6
+ module HowIs
7
+ # Provides basic templating functionality.
8
+ Template = Okay::Template.new(File.expand_path("./templates/", __dir__))
9
+ end
@@ -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,5 @@
1
+ <p>There %{was_were} %{number_of_new_contributors} new contributor%{contributor_s} during this report period.</p>
2
+
3
+ <ul>
4
+ %{list_items}
5
+ </ul>
@@ -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>