how_is 19.0.0 → 20.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 (48) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.rubocop.yml +16 -0
  4. data/CHANGELOG.md +30 -3
  5. data/Gemfile +0 -6
  6. data/README.md +5 -2
  7. data/exe/how_is +7 -2
  8. data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +446 -727
  9. data/fixtures/vcr_cassettes/how-is-example-repository.yml +508 -727
  10. data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +43522 -1218
  11. data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +287 -0
  12. data/fixtures/vcr_cassettes/how-is-with-config-file.yml +44307 -23286
  13. data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +307 -0
  14. data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +81 -0
  15. data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +307 -0
  16. data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +307 -0
  17. data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +307 -0
  18. data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +74 -0
  19. data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +307 -0
  20. data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +234 -0
  21. data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +378 -0
  22. data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +378 -0
  23. data/fixtures/vcr_cassettes/how_is_fetcher_call.yml +572 -0
  24. data/how_is.gemspec +3 -2
  25. data/lib/how_is/cli.rb +4 -6
  26. data/lib/how_is/frontmatter.rb +46 -0
  27. data/lib/how_is/report.rb +59 -63
  28. data/lib/how_is/sources/github/contributions.rb +164 -0
  29. data/lib/how_is/sources/github/issues.rb +142 -0
  30. data/lib/how_is/sources/github/pulls.rb +20 -0
  31. data/lib/how_is/sources/github.rb +11 -0
  32. data/lib/how_is/sources/github_helpers.rb +191 -0
  33. data/lib/how_is/sources/travis.rb +34 -0
  34. data/lib/how_is/sources.rb +9 -0
  35. data/lib/how_is/templates/issues_or_pulls_partial.html_template +7 -0
  36. data/lib/how_is/templates/report.html_template +27 -0
  37. data/lib/how_is/templates/report_partial.html_template +12 -0
  38. data/lib/how_is/version.rb +2 -2
  39. data/lib/how_is.rb +59 -158
  40. metadata +42 -16
  41. data/lib/how_is/analyzer.rb +0 -222
  42. data/lib/how_is/builds.rb +0 -36
  43. data/lib/how_is/contributions.rb +0 -156
  44. data/lib/how_is/fetcher.rb +0 -77
  45. data/lib/how_is/pulse.rb +0 -47
  46. data/lib/how_is/report/base_report.rb +0 -148
  47. data/lib/how_is/report/html.rb +0 -120
  48. data/lib/how_is/report/json.rb +0 -34
@@ -1,222 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "contracts"
4
- require "ostruct"
5
- require "date"
6
- require "json"
7
-
8
- class HowIs
9
- ##
10
- # Represents a completed analysis of the repository being analyzed.
11
- class Analysis < OpenStruct
12
- end
13
-
14
- # Creates Analysis objects with input data formatted in useful ways.
15
- class Analyzer
16
- include Contracts::Core
17
-
18
- ##
19
- # Raised when attempting to import to an unsupported format.
20
- class UnsupportedImportFormat < StandardError
21
- def initialize(format)
22
- super("Unsupported import format: #{format}")
23
- end
24
- end
25
-
26
- ##
27
- # Generates and returns an analysis.
28
- #
29
- # @param data [Fetcher::Results] The results gathered by Fetcher.
30
- # @param analysis_class (You don't need this.) A class to use instead of
31
- # HowIs::Analysis.
32
- Contract Fetcher::Results, C::KeywordArgs[analysis_class: C::Optional[Class]] => Analysis
33
- def call(data, analysis_class: Analysis)
34
- issues = data.issues
35
- pulls = data.pulls
36
-
37
- analysis_class.new(
38
- issues_url: "https://github.com/#{data.repository}/issues",
39
- pulls_url: "https://github.com/#{data.repository}/pulls",
40
-
41
- repository: data.repository,
42
-
43
- number_of_issues: issues.length,
44
- number_of_pulls: pulls.length,
45
-
46
- issues_with_label: with_label_links(num_with_label(issues), data.repository),
47
- issues_with_no_label: {"link" => nil, "total" => num_with_no_label(issues)},
48
-
49
- average_issue_age: average_age_for(issues),
50
- average_pull_age: average_age_for(pulls),
51
-
52
- oldest_issue: issue_or_pull_to_hash(oldest_for(issues)),
53
- oldest_pull: issue_or_pull_to_hash(oldest_for(pulls)),
54
-
55
- newest_issue: issue_or_pull_to_hash(newest_for(issues)),
56
- newest_pull: issue_or_pull_to_hash(newest_for(pulls)),
57
-
58
- pulse: data.pulse
59
- )
60
- end
61
-
62
- ##
63
- # Generates an analysis from a hash of report data.
64
- #
65
- # @param data [Hash] The hash to generate an Analysis from.
66
- def self.from_hash(data)
67
- hash = data.map { |k, v|
68
- v = DateTime.parse(v) if k.end_with?("_date")
69
-
70
- [k, v]
71
- }.to_h
72
-
73
- hash.keys.each do |key|
74
- next unless hash[key].is_a?(Hash) && hash[key]["date"]
75
-
76
- hash[key]["date"] = DateTime.parse(hash[key]["date"])
77
- end
78
-
79
- Analysis.new(hash)
80
- end
81
-
82
- # Given an Array of issues or pulls, return a Hash specifying how many
83
- # issues or pulls use each label.
84
- def num_with_label(issues_or_pulls)
85
- # Returned hash maps labels to frequency.
86
- # E.g., given 10 issues/pulls with label "label1" and 5 with label "label2",
87
- # {
88
- # "label1" => 10,
89
- # "label2" => 5
90
- # }
91
-
92
- hash = Hash.new(0)
93
- issues_or_pulls.each do |iop|
94
- next unless iop["labels"]
95
-
96
- iop["labels"].each do |label|
97
- hash[label["name"]] += 1
98
- end
99
- end
100
- hash
101
- end
102
-
103
- # Returns the number of issues with no label.
104
- def num_with_no_label(issues)
105
- issues.select { |x| x["labels"].empty? }.length
106
- end
107
-
108
- # Given an Array of dates, average the timestamps and return the date that
109
- # represents.
110
- def average_date_for(issues_or_pulls)
111
- timestamps = issues_or_pulls.map { |iop| Date.parse(iop["created_at"]).strftime("%s").to_i }
112
- average_timestamp = timestamps.reduce(:+) / issues_or_pulls.length
113
-
114
- DateTime.strptime(average_timestamp.to_s, "%s")
115
- end
116
-
117
- # Given an Array of issues or pulls, return the average age of them.
118
- # Returns nil if no issues or pulls are provided.
119
- def average_age_for(issues_or_pulls)
120
- return nil if issues_or_pulls.empty?
121
-
122
- ages = issues_or_pulls.map { |iop| time_ago_in_seconds(iop["created_at"]) }
123
- average_age_in_seconds = ages.reduce(:+) / ages.length
124
-
125
- values = period_pairs_for(average_age_in_seconds).reject { |(v, _)| v.zero? }.map { |(v, k)|
126
- k += "s" if v != 1
127
- [v, k]
128
- }
129
-
130
- most_significant = values[0, 2].map { |x| x.join(" ") }
131
-
132
- value =
133
- if most_significant.length < 2
134
- most_significant.first
135
- else
136
- most_significant.join(" and ")
137
- end
138
-
139
- "approximately #{value}"
140
- end
141
-
142
- def sort_iops_by_created_at(issues_or_pulls)
143
- issues_or_pulls.sort_by { |x| DateTime.parse(x["created_at"]) }
144
- end
145
-
146
- # Given an Array of issues or pulls, return the oldest.
147
- # Returns nil if no issues or pulls are provided.
148
- def oldest_for(issues_or_pulls)
149
- return nil if issues_or_pulls.empty?
150
-
151
- sort_iops_by_created_at(issues_or_pulls).first
152
- end
153
-
154
- # Given an Array of issues or pulls, return the newest.
155
- # Returns nil if no issues or pulls are provided.
156
- def newest_for(issues_or_pulls)
157
- return nil if issues_or_pulls.empty?
158
-
159
- sort_iops_by_created_at(issues_or_pulls).last
160
- end
161
-
162
- # Given an issue or PR, returns the date it was created.
163
- def date_for(issue_or_pull)
164
- DateTime.parse(issue_or_pull["created_at"])
165
- end
166
-
167
- private
168
-
169
- # Takes an Array of labels, and returns amodified list that includes links
170
- # to each label.
171
- def with_label_links(labels, repository)
172
- labels.map { |label, num_issues|
173
- label_link = "https://github.com/#{repository}/issues?q=" + CGI.escape("is:open is:issue label:\"#{label}\"")
174
-
175
- [label, {"link" => label_link, "total" => num_issues}]
176
- }.to_h
177
- end
178
-
179
- # Returns how many seconds ago a date (as a String) was.
180
- def time_ago_in_seconds(x)
181
- DateTime.now.strftime("%s").to_i - DateTime.parse(x).strftime("%s").to_i
182
- end
183
-
184
- def issue_or_pull_to_hash(iop)
185
- return nil if iop.nil?
186
-
187
- ret = {}
188
-
189
- ret["html_url"] = iop["html_url"]
190
- ret["number"] = iop["number"]
191
- ret["date"] = date_for(iop)
192
-
193
- ret
194
- end
195
-
196
- SECONDS_IN_A_YEAR = 31_556_926
197
- SECONDS_IN_A_MONTH = 2_629_743
198
- SECONDS_IN_A_WEEK = 604_800
199
- SECONDS_IN_A_DAY = 86_400
200
-
201
- # Calculates a list of pairs of value and period label.
202
- #
203
- # @param age_in_seconds [Float]
204
- #
205
- # @return [Array<Array>] The input age_in_seconds expressed as different
206
- # units, as pairs of value and unit name.
207
- def period_pairs_for(age_in_seconds)
208
- years_remainder = age_in_seconds % SECONDS_IN_A_YEAR
209
-
210
- months_remainder = years_remainder % SECONDS_IN_A_MONTH
211
-
212
- weeks_remainder = months_remainder % SECONDS_IN_A_WEEK
213
-
214
- [
215
- [age_in_seconds / SECONDS_IN_A_YEAR, "year"],
216
- [years_remainder / SECONDS_IN_A_MONTH, "month"],
217
- [months_remainder / SECONDS_IN_A_WEEK, "week"],
218
- [weeks_remainder / SECONDS_IN_A_DAY, "day"],
219
- ]
220
- end
221
- end
222
- end
data/lib/how_is/builds.rb DELETED
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "how_is/fetcher"
4
-
5
- class HowIs
6
- # Fetches metadata about CI builds.
7
- #
8
- # Supports Travis
9
- class Builds
10
- # @param user [String] GitHub user of repository.
11
- # @param repo [String] GitHub repository name.
12
- def initialize(user:, repo:)
13
- @user = user
14
- @repo = repo
15
- # TODO: Figure out Default Branch of the repo
16
- end
17
-
18
- def summary
19
- JSON.parse(travis_builds)
20
- end
21
-
22
- # Returns API result of /repos/:user/:repo/builds for Push type Travis
23
- # events.
24
- #
25
- # @return [String] JSON result
26
- def travis_builds
27
- Tessellator::Fetcher::Request::HTTP.call(
28
- Tessellator::Fetcher::Config.new,
29
- "get",
30
- "http://api.travis-ci.org/repos/#{@user}/#{@repo}/builds?event_type=push",
31
- {},
32
- headers: {"Accept" => "application/vnd.travis-ci.2+json"}
33
- ).body
34
- end
35
- end
36
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "how_is/fetcher"
4
-
5
- class HowIs
6
- # Fetch information about who has contributed to a repository during a given
7
- # period.
8
- #
9
- # Usage:
10
- #
11
- # github = Github.new()
12
- # c = HowIs::Contributions.new(github: github, start_date: '2017-07-01', user: 'how-is', repo: 'how_is')
13
- # c.commits #=> All commits during July 2017.
14
- # c.contributors #=> All contributors during July 2017.
15
- # c.new_contributors #=> New contributors during July 2017.
16
- class Contributions
17
- # Returns an object that fetches contributor information about a particular
18
- # repository for a month-long period starting on +start_date+.
19
- #
20
- # @param github [Github] Github client instance.
21
- # @param start_date [String] Date in the format YYYY-MM-DD. The first date
22
- # to include commits from.
23
- # @param user [String] GitHub user of repository.
24
- # @param repo [String] GitHub repository name.
25
- def initialize(github: Fetcher.default_github_instance, start_date:, user:, repo:)
26
- @github = github
27
-
28
- # IMPL. DETAIL: The external API uses "start_date" so it's clearer,
29
- # but internally we use "since_date" to match GitHub's API.
30
-
31
- @since_date = Date.strptime(start_date, "%Y-%m-%d")
32
-
33
- d = @since_date.day
34
- m = @since_date.month
35
- y = @since_date.year
36
- @until_date = Date.new(y, m + 1, d)
37
-
38
- @user = user
39
- @repo = repo
40
-
41
- @commit = {}
42
- end
43
-
44
- # Returns a list of contributors that have zero commits before the @since_date.
45
- #
46
- # @return [Hash{String => Hash}] Contributors keyed by email
47
- def new_contributors
48
- # author: GitHub login, name or email by which to filter by commit author.
49
- @new_contributors ||= contributors.select do |email, _committer|
50
- # Returns true if +email+ never wrote a commit for +@repo+ before +@since_date+.
51
- @github.repos.commits.list(user: @user,
52
- repo: @repo,
53
- until: @since_date,
54
- author: email).count.zero?
55
- end
56
- end
57
-
58
- # @return [Hash{String => Hash}] Author information keyed by author's email.
59
- def contributors
60
- commits.map { |api_response|
61
- [api_response.commit.author.email, api_response.commit.author.to_h]
62
- }.to_h
63
- end
64
-
65
- def commits
66
- @commits ||= begin
67
- @github.repos.commits.list(user: @user,
68
- repo: @repo,
69
- since: @since_date).map { |c|
70
- # The commits list endpoint doesn't include all commit data, e.g. stats.
71
- # So, we make N requests here, where N == number of commits returned,
72
- # and then we die a bit inside.
73
- commit(c.sha)
74
- }
75
- end
76
- end
77
-
78
- def commit(sha)
79
- @commit[sha] ||= @github.repos.commits.get(user: @user, repo: @repo, sha: sha)
80
- end
81
-
82
- def changes
83
- if @stats.nil? || @changed_files.nil?
84
- @stats = {
85
- "total" => 0,
86
- "additions" => 0,
87
- "deletions" => 0,
88
- }
89
-
90
- @changed_files = []
91
-
92
- commits.map do |commit|
93
- commit.stats.each do |k, v|
94
- @stats[k] += v
95
- end
96
-
97
- @changed_files += commit.files.map { |file| file["filename"] }
98
- end
99
-
100
- @changed_files.sort.uniq!
101
- end
102
-
103
- {"stats" => @stats, "files" => @changed_files}
104
- end
105
-
106
- def changed_files
107
- changes["files"]
108
- end
109
-
110
- def additions_count
111
- changes["stats"]["additions"]
112
- end
113
-
114
- def deletions_count
115
- changes["stats"]["deletions"]
116
- end
117
-
118
- def compare_url
119
- since_timestamp = @since_date.to_time.to_i
120
- until_timestamp = @until_date.to_time.to_i
121
- "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{since_timestamp}%7D...#{default_branch}@%7B#{until_timestamp}%7D" # rubocop:disable Metrics/LineLength
122
- end
123
-
124
- def default_branch
125
- @default_branch ||= @github.repos.get(user: @user,
126
- repo: @repo).default_branch
127
- end
128
-
129
- def summary(start_text: nil)
130
- # TODO: Pulse has information about _all_ branches. Do we want that?
131
- # If we do, we'd need to pass a branch name as the 'sha' parameter
132
- # to /repos/:owner/:repo/commits.
133
- # https://developer.github.com/v3/repos/commits/
134
-
135
- start_text ||= "From #{pretty_date(@since_date)} through #{pretty_date(@until_date)}"
136
-
137
- "#{start_text}, #{@user}/#{@repo} gained "\
138
- "<a href=\"#{compare_url}\">#{pluralize('new commit', commits.length)}</a>, " \
139
- "contributed by #{pluralize('author', contributors.length)}. There " \
140
- "#{(additions_count == 1) ? 'was' : 'were'} " \
141
- "#{pluralize('addition', additions_count)} and " \
142
- "#{pluralize('deletion', deletions_count)} across " \
143
- "#{pluralize('file', changed_files.length)}."
144
- end
145
-
146
- private
147
-
148
- def pretty_date(date)
149
- date.strftime("%b %d, %Y")
150
- end
151
-
152
- def pluralize(string, number)
153
- "#{number} #{string}#{(number == 1) ? '' : 's'}"
154
- end
155
- end
156
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "contracts"
4
- require "github_api"
5
- require "how_is/pulse"
6
-
7
- C ||= Contracts
8
-
9
- class HowIs
10
- ##
11
- # Fetches data from GitHub.
12
- class Fetcher
13
- include Contracts::Core
14
-
15
- # TODO: Fix this bullshit.
16
- # :nodoc:
17
- def self.default_github_instance
18
- Github.new(auto_pagination: true) do |config|
19
- config.basic_auth = ENV["HOWIS_BASIC_AUTH"] if ENV["HOWIS_BASIC_AUTH"]
20
- end
21
- end
22
-
23
- ##
24
- # Standardized representation for fetcher results.
25
- #
26
- # Implemented as a class instead of passing around a Hash so that it can
27
- # be more easily referenced by Contracts.
28
- Results = Struct.new(:repository, :issues, :pulls, :pulse) do
29
- include Contracts::Core
30
-
31
- Contract String, C::ArrayOf[Hash], C::ArrayOf[Hash], String => nil
32
- def initialize(repository, issues, pulls, pulse)
33
- super(repository, issues, pulls, pulse)
34
- end
35
-
36
- # Struct defines #to_h, but not #to_hash, so we alias them.
37
- alias_method :to_hash, :to_h
38
- end
39
-
40
- ##
41
- # Fetches repository information from GitHub and returns a Results object.
42
- Contract String,
43
- C::Or[C::RespondTo[:issues, :pulls], nil],
44
- C::Or[C::RespondTo[:html_summary], nil] => Results
45
- def call(repository,
46
- github = nil,
47
- pulse = nil)
48
- github ||= self.class.default_github_instance
49
- pulse ||= HowIs::Pulse.new(repository)
50
- user, repo = repository.split("/", 2)
51
-
52
- unless user && repo
53
- raise HowIs::CLI::OptionsError, "To generate a report from GitHub, " \
54
- "provide the repository " \
55
- "username/project. Quitting!"
56
- end
57
-
58
- issues = github.issues.list user: user, repo: repo
59
- pulls = github.pulls.list user: user, repo: repo
60
-
61
- summary = pulse.html_summary
62
-
63
- Results.new(
64
- repository,
65
- obj_to_array_of_hashes(issues),
66
- obj_to_array_of_hashes(pulls),
67
- summary
68
- )
69
- end
70
-
71
- private
72
-
73
- def obj_to_array_of_hashes(object)
74
- object.to_a.map(&:to_h)
75
- end
76
- end
77
- end
data/lib/how_is/pulse.rb DELETED
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tessellator/fetcher"
4
-
5
- class HowIs
6
- # This entire class is a monstrous hack, because GitHub doesn't provide
7
- # a good API for Pulse.
8
- #
9
- # TODO: Use GitHub's Statistics API to replace this garbage.
10
- # See https://github.com/how-is/how_is/issues/122
11
- class Pulse
12
- def initialize(repository)
13
- @repository = repository
14
- @pulse_page_response = fetch_pulse!(repository)
15
- end
16
-
17
- # Gets the HTML Pulse summary.
18
- def html_summary
19
- if stats_section?
20
- stats_html_fragment.gsub('<a href="/', '<a href="https://github.com/')
21
- else
22
- "There hasn't been any activity on #{@repository} in the last month."
23
- end
24
- end
25
-
26
- private
27
-
28
- HTML_SEPARATOR_FOR_STATS = '<div class="section diffstat-summary">'
29
-
30
- def stats_section?
31
- parts.count > 1
32
- end
33
-
34
- def parts
35
- @parts ||= @pulse_page_response.body.split(HTML_SEPARATOR_FOR_STATS)
36
- end
37
-
38
- def stats_html_fragment
39
- parts.last.split("</div>").first.strip
40
- end
41
-
42
- # Fetch Pulse page from GitHub for scraping.
43
- def fetch_pulse!(repository)
44
- Tessellator::Fetcher.new.call("get", "https://github.com/#{repository}/pulse/monthly")
45
- end
46
- end
47
- end
@@ -1,148 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- class HowIs
6
- BaseReport = Struct.new(:analysis)
7
-
8
- ##
9
- # Subclasses of BaseReport represent complete reports.
10
- class BaseReport
11
- def generate_report_text!
12
- # title, text, header, horizontal_bar_graph, etc,
13
- # append to @r, which is returned at the end of the function.
14
-
15
- title "How is #{analysis.repository}?"
16
-
17
- # DateTime#new_offset(0) sets the timezone to UTC. I think it does this
18
- # without changing anything besides the timezone, but who knows, 'cause
19
- # new_offset is entirely undocumented! (Even though it's used in the
20
- # DateTime documentation!)
21
- #
22
- # TODO: Stop pretending everyone who runs how_is is in UTC.
23
- text "Monthly report, ending on #{DateTime.now.new_offset(0).strftime('%B %e, %Y')}."
24
-
25
- text analysis.pulse
26
-
27
- header "Pull Requests"
28
- issue_or_pr_summary "pull", "pull request"
29
-
30
- header "Issues"
31
- issue_or_pr_summary "issue", "issue"
32
-
33
- header "Issues Per Label"
34
- issues_per_label = analysis.issues_with_label.to_a.sort_by { |(_, v)| v["total"].to_i }.reverse
35
- issues_per_label.map! do |label, hash|
36
- [label, hash["total"], hash["link"]]
37
- end
38
- issues_per_label << ["(No label)", analysis.issues_with_no_label["total"], nil]
39
- horizontal_bar_graph issues_per_label
40
-
41
- # See comment at beginning of function.
42
- @r
43
- end
44
-
45
- # === Methods implemented by subclasses of BaseReport ===
46
-
47
- ##
48
- # Returns the format of report this class generates.
49
- #
50
- # @return [Symbol] A lowercase symbol denoting the report format.
51
- def format
52
- raise NotImplementedError
53
- end
54
-
55
- ##
56
- # Appends a title to the report.
57
- def title(_content)
58
- raise NotImplementedError
59
- end
60
-
61
- ##
62
- # Appends a header to the report.
63
- def header(_content)
64
- raise NotImplementedError
65
- end
66
-
67
- ##
68
- # Appends a line of text to the report.
69
- def text(_content)
70
- raise NotImplementedError
71
- end
72
-
73
- ##
74
- # Appends a link to the report.
75
- def link(_content, _url)
76
- raise NotImplementedError
77
- end
78
-
79
- ##
80
- # Appends an unordered list to the report.
81
- def unordered_list(_arr)
82
- raise NotImplementedError
83
- end
84
-
85
- ##
86
- # Appends a horizontal bar graph to the report.
87
- def horizontal_bar_graph(_data)
88
- raise NotImplementedError
89
- end
90
-
91
- ##
92
- # Exports the report as a String.
93
- def export
94
- raise NotImplementedError
95
- end
96
-
97
- ##
98
- # Exports a report to a file.
99
- #
100
- # NOTE: May be removed in the future.
101
- def export_file(_file)
102
- raise NotImplementedError
103
- end
104
-
105
- def to_h
106
- analysis.to_h
107
- end
108
- alias_method :to_hash, :to_h
109
-
110
- def to_json
111
- JSON.pretty_generate(to_h)
112
- end
113
-
114
- private
115
-
116
- def pluralize(text, number)
117
- (number == 1) ? text : "#{text}s"
118
- end
119
-
120
- def are_is(number)
121
- (number == 1) ? "is" : "are"
122
- end
123
-
124
- def issue_or_pr_summary(type, type_label)
125
- date_format = "%b %e, %Y"
126
- a = analysis
127
-
128
- number_of_type = a.public_send("number_of_#{type}s")
129
-
130
- type_link = a.public_send("#{type}s_url")
131
- oldest = a.public_send("oldest_#{type}")
132
- newest = a.public_send("newest_#{type}")
133
-
134
- if number_of_type.zero?
135
- text "There are #{link("no #{type_label}s open", type_link)}."
136
- else
137
- text "There #{are_is(number_of_type)} #{link("#{number_of_type} "\
138
- "#{pluralize(type_label, number_of_type)} open", type_link)}."
139
-
140
- unordered_list [
141
- "Average age: #{a.public_send("average_#{type}_age")}.",
142
- "#{link('Oldest ' + type_label, oldest['html_url'])} was opened on #{oldest['date'].strftime(date_format)}.",
143
- "#{link('Newest ' + type_label, newest['html_url'])} was opened on #{newest['date'].strftime(date_format)}.",
144
- ]
145
- end
146
- end
147
- end
148
- end