inq 26.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.cirrus.yml +84 -0
  3. data/.codeclimate.yml +23 -0
  4. data/.github_changelog_generator +2 -0
  5. data/.gitignore +15 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +260 -0
  8. data/.travis.yml +24 -0
  9. data/CHANGELOG.md +499 -0
  10. data/CODE_OF_CONDUCT.md +49 -0
  11. data/CONTRIBUTING.md +34 -0
  12. data/Gemfile +15 -0
  13. data/ISSUES.md +62 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +91 -0
  16. data/Rakefile +76 -0
  17. data/bin/console +14 -0
  18. data/bin/prerelease-generate-changelog +28 -0
  19. data/bin/setup +8 -0
  20. data/bors.toml +17 -0
  21. data/build-debug.rb +20 -0
  22. data/exe/inq +7 -0
  23. data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +597 -0
  24. data/fixtures/vcr_cassettes/how-is-example-repository.yml +768 -0
  25. data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +23940 -0
  26. data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +66 -0
  27. data/fixtures/vcr_cassettes/how-is-with-config-file.yml +23940 -0
  28. data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +247 -0
  29. data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +247 -0
  30. data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +247 -0
  31. data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +247 -0
  32. data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +247 -0
  33. data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +81 -0
  34. data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +81 -0
  35. data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +247 -0
  36. data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +402 -0
  37. data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +325 -0
  38. data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +325 -0
  39. data/inq.gemspec +45 -0
  40. data/lib/inq.rb +63 -0
  41. data/lib/inq/cacheable.rb +71 -0
  42. data/lib/inq/cli.rb +135 -0
  43. data/lib/inq/config.rb +123 -0
  44. data/lib/inq/constants.rb +9 -0
  45. data/lib/inq/date_time_helpers.rb +48 -0
  46. data/lib/inq/exe.rb +66 -0
  47. data/lib/inq/frontmatter.rb +51 -0
  48. data/lib/inq/report.rb +140 -0
  49. data/lib/inq/report_collection.rb +113 -0
  50. data/lib/inq/sources.rb +11 -0
  51. data/lib/inq/sources/ci/appveyor.rb +87 -0
  52. data/lib/inq/sources/ci/travis.rb +159 -0
  53. data/lib/inq/sources/github.rb +57 -0
  54. data/lib/inq/sources/github/contributions.rb +204 -0
  55. data/lib/inq/sources/github/issue_fetcher.rb +148 -0
  56. data/lib/inq/sources/github/issues.rb +126 -0
  57. data/lib/inq/sources/github/pulls.rb +29 -0
  58. data/lib/inq/sources/github_helpers.rb +106 -0
  59. data/lib/inq/template.rb +9 -0
  60. data/lib/inq/templates/contributions_partial.html +1 -0
  61. data/lib/inq/templates/issues_or_pulls_partial.html +7 -0
  62. data/lib/inq/templates/new_contributors_partial.html +5 -0
  63. data/lib/inq/templates/report.html +19 -0
  64. data/lib/inq/templates/report_partial.html +12 -0
  65. data/lib/inq/text.rb +26 -0
  66. data/lib/inq/version.rb +6 -0
  67. metadata +263 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/report"
4
+ require "okay/warning_helpers"
5
+
6
+ module Inq
7
+ ##
8
+ # A class representing a collection of Reports.
9
+ class ReportCollection
10
+ include Okay::WarningHelpers
11
+
12
+ def initialize(config, date)
13
+ @config = config
14
+
15
+ # If the config is in the old format, convert it to the new one.
16
+ unless @config["repositories"]
17
+ @config["repositories"] = [{
18
+ "repository" => @config.delete("repository"),
19
+ "reports" => @config.delete("reports"),
20
+ }]
21
+ end
22
+
23
+ @date = date
24
+ @reports = config["repositories"].map(&method(:fetch_report)).to_h
25
+ end
26
+
27
+ # Generates the metadata for the collection of Reports.
28
+ def metadata(repository)
29
+ end_date = DateTime.strptime(@date, "%Y-%m-%d")
30
+ friendly_end_date = end_date.strftime("%B %d, %y")
31
+
32
+ {
33
+ sanitized_repository: repository.tr("/", "-"),
34
+ repository: repository,
35
+ date: end_date,
36
+ friendly_date: friendly_end_date,
37
+ }
38
+ end
39
+ private :metadata
40
+
41
+ def config_for(repo)
42
+ defaults = @config.fetch("default_reports", {})
43
+ config = @config.dup
44
+ repos = config.delete("repositories")
45
+
46
+ # Find the _last_ one that matches, to allow overriding.
47
+ repo_config = repos.reverse.find { |conf| conf["repository"] == repo }
48
+
49
+ # Use values from default_reports, unless overridden.
50
+ config["repository"] = repo
51
+ config["reports"] = defaults.merge(repo_config.fetch("reports", {}))
52
+ config
53
+ end
54
+ private :config_for
55
+
56
+ def fetch_report(repo_config)
57
+ repo = repo_config["repository"]
58
+ report = Report.new(config_for(repo), @date)
59
+ [repo, report]
60
+ end
61
+ private :fetch_report
62
+
63
+ # Converts a ReportCollection to a Hash.
64
+ #
65
+ # Also good for giving programmers nightmares, I suspect.
66
+ def to_h
67
+ results = {}
68
+ defaults = @config["default_reports"] || {}
69
+
70
+ @config["repositories"].map { |repo_config|
71
+ repo = repo_config["repository"]
72
+ config = config_for(repo)
73
+
74
+ config["reports"].map { |format, report_config|
75
+ # Sometimes report_data has unused keys, which generates a warning, but
76
+ # we're okay with it, so we wrap it with silence_warnings {}.
77
+ filename = silence_warnings {
78
+ tmp_filename = report_config["filename"] || defaults[format]["filename"]
79
+ tmp_filename % metadata(repo)
80
+ }
81
+
82
+ directory = report_config["directory"] || defaults[format]["directory"]
83
+ file = File.join(directory, filename)
84
+
85
+ # Export +report+ to the specified +format+ with the specified
86
+ # +frontmatter+.
87
+ frontmatter = report_config["frontmatter"] || {}
88
+ if defaults.has_key?(format) && defaults[format].has_key?("frontmatter")
89
+ frontmatter = defaults[format]["frontmatter"].merge(frontmatter)
90
+ end
91
+ frontmatter = nil if frontmatter == {}
92
+
93
+ export = @reports[repo].send("to_#{format}", frontmatter)
94
+
95
+ results[file] = export
96
+ }
97
+ }
98
+ results
99
+ end
100
+
101
+ # Save all of the reports to the corresponding files.
102
+ #
103
+ # @return [Array<String>] An array of file paths.
104
+ def save_all
105
+ reports = to_h
106
+ reports.each do |file, report|
107
+ File.write(file, report)
108
+ end
109
+
110
+ reports.keys
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/version"
4
+
5
+ module Inq
6
+ ##
7
+ # Various information sources used by Inq.
8
+ module Sources
9
+ # Simply for creating a namespace.
10
+ end
11
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "okay/default"
4
+ require "okay/http"
5
+ require "inq/constants"
6
+ require "inq/sources"
7
+ require "inq/sources/github/contributions"
8
+ require "inq/text"
9
+
10
+ module Inq
11
+ module Sources
12
+ module CI
13
+ # Fetches metadata about CI builds from appveyor.com.
14
+ class Appveyor
15
+ # @param repository [String] GitHub repository name, of the format user/repo.
16
+ # @param start_date [String] Start date for the report being generated.
17
+ # @param end_date [String] End date for the report being generated.
18
+ # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls
19
+ def initialize(config, start_date, end_date, cache)
20
+ @config = config
21
+ @cache = cache
22
+ @repository = config["repository"]
23
+ @start_date = DateTime.parse(start_date)
24
+ @end_date = DateTime.parse(end_date)
25
+ @default_branch = Okay.default
26
+ end
27
+
28
+ # @return [String] The default branch name.
29
+ def default_branch
30
+ return @default_branch unless @default_branch.nil?
31
+
32
+ contributions = Sources::GitHub::Contributions.new(@config, nil, nil)
33
+
34
+ @default_branch = contributions.default_branch
35
+ end
36
+
37
+ # Fetches builds for the default branch.
38
+ #
39
+ # @return [Hash] Builds for the default branch.
40
+ def builds
41
+ @builds ||=
42
+ fetch_builds["builds"] \
43
+ .map(&method(:normalize_build)) \
44
+ .select(&method(:in_date_range?))
45
+ rescue Net::HTTPServerException
46
+ # It's not elegant, but it works™.
47
+ []
48
+ end
49
+
50
+ private
51
+
52
+ def in_date_range?(build)
53
+ build["started_at"] >= @start_date &&
54
+ build["started_at"] <= @end_date
55
+ end
56
+
57
+ def normalize_build(build)
58
+ build["started_at"] = DateTime.parse(build["created"])
59
+ build["html_url"] = "https://ci.appveyor.com/project/#{@repository}/build/#{build['buildNumber']}"
60
+ build
61
+ end
62
+
63
+ # Returns API result of /api/projects/:repository.
64
+ # FIXME: This doesn't limit results based on the date range.
65
+ #
66
+ # @return [Hash] API results.
67
+ def fetch_builds
68
+ @cache.cached("appveyor_builds") do
69
+ Inq::Text.print "Fetching Appveyor build data."
70
+
71
+ ret = Okay::HTTP.get(
72
+ "https://ci.appveyor.com/api/projects/#{@repository}/history",
73
+ parameters: {"recordsNumber" => "100"},
74
+ headers: {
75
+ "Accept" => "application/json",
76
+ "User-Agent" => Inq::USER_AGENT,
77
+ }
78
+ ).or_raise!.from_json
79
+
80
+ Inq::Text.puts
81
+ ret
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "okay/default"
5
+ require "okay/http"
6
+ require "inq/constants"
7
+ require "inq/sources/github"
8
+ require "inq/text"
9
+
10
+ module Inq
11
+ module Sources
12
+ module CI
13
+ # Fetches metadata about CI builds from travis-ci.org.
14
+ class Travis
15
+ BadResponseError = Class.new(StandardError)
16
+
17
+ # @param repository [String] GitHub repository name, of the format user/repo.
18
+ # @param start_date [String] Start date for the report being generated.
19
+ # @param end_date [String] End date for the report being generated.
20
+ # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls
21
+ def initialize(config, start_date, end_date, cache)
22
+ @config = config
23
+ @cache = cache
24
+ @repository = config["repository"]
25
+ raise "Travis.new() got nil repository." if @repository.nil?
26
+ @start_date = DateTime.parse(start_date)
27
+ @end_date = DateTime.parse(end_date)
28
+ @default_branch = Okay.default
29
+ end
30
+
31
+ # @return [String] The default branch name.
32
+ def default_branch
33
+ return @default_branch unless @default_branch == Okay.default
34
+
35
+ response = fetch("branches", {"sort_by" => "default_branch"})
36
+ validate_response!(response)
37
+
38
+ branches = response["branches"]
39
+ validate_branches!(branches)
40
+
41
+ branch = branches.find { |b| b["default_branch"] == true }
42
+ @default_branch = branch&.fetch("name", nil)
43
+ end
44
+
45
+ # Returns the builds for the default branch.
46
+ #
47
+ # @return [Hash] Hash containing the builds for the default branch.
48
+ def builds
49
+ @cache.cached("travis_builds") do
50
+ raw_builds \
51
+ .map(&method(:normalize_build)) \
52
+ .select(&method(:in_date_range?)) \
53
+ .map(&method(:add_build_urls))
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def add_build_urls(build)
60
+ build["html_url"] = "https://travis-ci.org/#{build['repository']}#{build['@href']}"
61
+ build
62
+ end
63
+
64
+ def validate_response!(response)
65
+ return true if hash_with_key?(response, "branches")
66
+
67
+ raise BadResponseError,
68
+ "expected `response' (#{response.class}) to be a Hash with key `\"branches\"'."
69
+ end
70
+
71
+ def validate_branches!(branches)
72
+ return true if array_of_hashes?(branches)
73
+
74
+ raise BadResponseError, "expected `branches' to be Array of Hashes."
75
+ end
76
+
77
+ def array_of_hashes?(ary)
78
+ ary.is_a?(Array) && ary.all? { |obj| obj.is_a?(Hash) }
79
+ end
80
+
81
+ def hash_with_key?(hsh, key)
82
+ hsh.is_a?(Hash) && hsh.has_key?(key)
83
+ end
84
+
85
+ def in_date_range?(build, start_date = @start_date, end_date = @end_date)
86
+ (build["started_at"] >= start_date) \
87
+ && (build["finished_at"] <= end_date)
88
+ end
89
+
90
+ def raw_builds
91
+ results = fetch("builds", {
92
+ "event_type" => "push",
93
+ "branch.name" => default_branch,
94
+ })
95
+
96
+ results["builds"] || {}
97
+ rescue Net::HTTPServerException
98
+ # It's not elegant, but it works™.
99
+ {}
100
+ end
101
+
102
+ def normalize_build(build)
103
+ build_keys = ["@href", "pull_request_title", "pull_request_number",
104
+ "started_at", "finished_at", "repository", "commit",
105
+ "jobs"]
106
+ result = pluck_keys(build, build_keys)
107
+
108
+ commit_keys = ["sha", "ref", "message", "compare_url",
109
+ "committed_at", "jobs"]
110
+ result["commit"] = pluck_keys(result["commit"], commit_keys)
111
+
112
+ job_keys = ["href", "id"]
113
+ result["jobs"] = result["jobs"].map { |j| pluck_keys(j, job_keys) }
114
+
115
+ result["repository"] = result["repository"]["slug"]
116
+
117
+ ["started_at", "finished_at"].each do |k|
118
+ next if k.nil?
119
+ result[k] = DateTime.parse(result[k])
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ def pluck_keys(hsh, keys)
126
+ keys.map { |k| [k, hsh[k]] }.to_h
127
+ end
128
+
129
+ # Returns API results for /repos/:user/:repo/<path>.
130
+ #
131
+ # @param path [String] Path suffix (appended to /repo/<repo name>/).
132
+ # @param parameters [Hash] Parameters.
133
+ # @return [String] JSON result.
134
+ def fetch(path, parameters = {})
135
+ @cache.cached("travis_path") do
136
+ Inq::Text.print "Fetching Travis CI #{path.sub(/e?s$/, '')} data."
137
+
138
+ # Apparently this is required for the Travis CI API to work.
139
+ repo = @repository.sub("/", "%2F")
140
+
141
+ ret = Okay::HTTP.get(
142
+ "https://api.travis-ci.org/repo/#{repo}/#{path}",
143
+ parameters: parameters,
144
+ headers: {
145
+ "Travis-Api-Version" => "3",
146
+ "Accept" => "application/json",
147
+ "User-Agent" => Inq::USER_AGENT,
148
+ }
149
+ ).or_raise!.from_json
150
+
151
+ Inq::Text.puts
152
+
153
+ ret
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inq/version"
4
+ require "inq/sources"
5
+ require "okay/graphql"
6
+
7
+ module Inq
8
+ module Sources
9
+ # Contains configuration information for GitHub-based sources.
10
+ class Github
11
+ class ConfigError < StandardError
12
+ end
13
+
14
+ def initialize(config)
15
+ must_have_key!(config, "sources/github")
16
+
17
+ @config = config["sources/github"]
18
+
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
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "github_api"
4
+ require "inq/cacheable"
5
+ require "inq/sources/github"
6
+ require "inq/sources/github_helpers"
7
+ require "inq/template"
8
+ require "inq/text"
9
+ require "date"
10
+
11
+ module Inq
12
+ module Sources
13
+ class Github
14
+ # Fetch information about who has contributed to a repository during
15
+ # a given period.
16
+ #
17
+ # Usage:
18
+ #
19
+ # c = Inq::Contributions.new(start_date: '2017-07-01', user: 'duckinator', repo: 'inq')
20
+ # c.commits #=> All commits during July 2017.
21
+ # c.contributors #=> All contributors during July 2017.
22
+ # c.new_contributors #=> New contributors during July 2017.
23
+ class Contributions
24
+ include Inq::Sources::GithubHelpers
25
+
26
+ # Returns an object that fetches contributor information about a
27
+ # particular repository for a month-long period starting on +start_date+.
28
+ #
29
+ # @param config [Hash] A config object.
30
+ # @param start_date [String] Date in the format YYYY-MM-DD.
31
+ # The first date to include commits from.
32
+ # @param end_date [String] Date in the format YYYY-MM-DD.
33
+ # The last date to include commits from.
34
+ # @param cache [Cacheable] Instance of Inq::Cacheable to cache API calls
35
+ def initialize(config, start_date, end_date, cache)
36
+ raise "Got String, need Hash. The Github::Contributions API changed." if config.is_a?(String)
37
+
38
+ @config = config
39
+ @cache = cache
40
+ @github = Sources::Github.new(config)
41
+ @repository = config["repository"]
42
+
43
+ @user, @repo = @repository.split("/")
44
+ @github = ::Github.new(auto_pagination: true) { |conf|
45
+ conf.basic_auth = @github.basic_auth
46
+ }
47
+
48
+ # IMPL. DETAIL: The external API uses "end_date" so it's clearer,
49
+ # but internally we use "until_date" to match GitHub's API.
50
+ @since_date = start_date
51
+ @until_date = end_date
52
+
53
+ @commit = {}
54
+ @stats = nil
55
+ @changed_files = nil
56
+ end
57
+
58
+ # Returns a list of contributors that have zero commits before the @since_date.
59
+ #
60
+ # @return [Hash{String => Hash}] Contributors keyed by email
61
+ def new_contributors
62
+ @new_contributors ||= contributors.select { |email, _committer|
63
+ args = {
64
+ user: @user,
65
+ repo: @repo,
66
+ until: @since_date,
67
+ author: email,
68
+ }
69
+ # True if +email+ never wrote a commit for +@repo+ before +@since_date+, false otherwise.
70
+ commits = @cache.cached("repos_commits", args.to_json) do
71
+ @github.repos.commits.list(**args)
72
+ end
73
+ commits.count.zero?
74
+ }
75
+ end
76
+
77
+ def new_contributors_html
78
+ names = new_contributors.values.map { |c| c["name"] }
79
+ list_items = names.map { |n| " <li>#{n}</li>" }.join("\n")
80
+
81
+ if names.length.zero?
82
+ num_new_contributors = "no"
83
+ else
84
+ num_new_contributors = names.length
85
+ end
86
+
87
+ if names.length == 1
88
+ was_were = "was"
89
+ contributor_s = ""
90
+ else
91
+ was_were = "were"
92
+ contributor_s = "s"
93
+ end
94
+
95
+ Template.apply("new_contributors_partial.html", {
96
+ was_were: was_were,
97
+ contributor_s: contributor_s,
98
+ number_of_new_contributors: num_new_contributors,
99
+ list_items: list_items,
100
+ }).strip
101
+ end
102
+
103
+ # @return [Hash{String => Hash}] Author information keyed by author's email.
104
+ def contributors
105
+ commits.map { |api_response|
106
+ [api_response.commit.author.email, api_response.commit.author.to_h]
107
+ }.to_h
108
+ end
109
+
110
+ def commits
111
+ return @commits if instance_variable_defined?(:@commits)
112
+
113
+ args = {
114
+ user: @user,
115
+ repo: @repo,
116
+ since: @since_date,
117
+ until: @until_date,
118
+ }
119
+
120
+ Inq::Text.print "Fetching #{@repository} commit data."
121
+
122
+ # The commits list endpoint doesn't include all stats.
123
+ # So, to compensate, we make N requests here, where N is number
124
+ # of commits returned, and then we die a bit inside.
125
+ @commits = @cache.cached("repos_commits", args.to_json) do
126
+ @github.repos.commits.list(**args).map { |c|
127
+ Inq::Text.print "."
128
+ commit(c.sha)
129
+ }
130
+ end
131
+ Inq::Text.puts
132
+ @commits
133
+ end
134
+
135
+ def commit(sha)
136
+ @commit[sha] ||= @cache.cached("repos_commit_#{sha}") do
137
+ @github.repos.commits.get(user: @user, repo: @repo, sha: sha)
138
+ end
139
+ end
140
+
141
+ def stats
142
+ return @stats if @stats
143
+
144
+ stats = {"total" => 0, "additions" => 0, "deletions" => 0}
145
+ commits.map do |commit|
146
+ stats.keys.each do |key|
147
+ stats[key] += commit.stats[key]
148
+ end
149
+ end
150
+ @stats = stats
151
+ end
152
+
153
+ def changed_files
154
+ return @changed_files if @changed_files
155
+ files = commits.flat_map do |commit|
156
+ commit.files.map { |file| file["filename"] }
157
+ end
158
+ @changed_files = files.sort.uniq
159
+ end
160
+
161
+ def changes
162
+ {"stats" => stats, "files" => changed_files}
163
+ end
164
+
165
+ def additions_count
166
+ changes["stats"]["additions"]
167
+ end
168
+
169
+ def deletions_count
170
+ changes["stats"]["deletions"]
171
+ end
172
+
173
+ def compare_url
174
+ "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{@since_date}%7D...#{default_branch}@%7B#{@until_date}%7D" # rubocop:disable Metrics/LineLength
175
+ end
176
+
177
+ def default_branch
178
+ @default_branch ||= @cache.cached("repos_default_branch") do
179
+ @github.repos.get(user: @user, repo: @repo).default_branch
180
+ end
181
+ end
182
+
183
+ # rubocop:disable Metrics/AbcSize
184
+ def to_html(start_text: nil)
185
+ start_text ||= "From #{pretty_date(@since_date)} through #{pretty_date(@until_date)}"
186
+
187
+ Inq::Template.apply("contributions_partial.html", {
188
+ start_text: start_text,
189
+ user: @user,
190
+ repo: @repo,
191
+ compare_url: compare_url,
192
+ additions_count_str: (additions_count == 1) ? "was" : "were",
193
+ authors: pluralize("author", contributors.length),
194
+ new_commits: pluralize("new commit", commits.length),
195
+ additions: pluralize("addition", additions_count),
196
+ deletions: pluralize("deletion", deletions_count),
197
+ changed_files: pluralize("file", changed_files.length),
198
+ }).strip
199
+ end
200
+ # rubocop:enable Metrics/AbcSize
201
+ end
202
+ end
203
+ end
204
+ end