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