how_is 19.0.0 → 20.0.0

Sign up to get free protection for your applications and to get access to all the features.
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