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.
- checksums.yaml +7 -0
- data/.cirrus.yml +84 -0
- data/.codeclimate.yml +23 -0
- data/.github_changelog_generator +2 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +260 -0
- data/.travis.yml +24 -0
- data/CHANGELOG.md +499 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +15 -0
- data/ISSUES.md +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +76 -0
- data/bin/console +14 -0
- data/bin/prerelease-generate-changelog +28 -0
- data/bin/setup +8 -0
- data/bors.toml +17 -0
- data/build-debug.rb +20 -0
- data/exe/inq +7 -0
- data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +597 -0
- data/fixtures/vcr_cassettes/how-is-example-repository.yml +768 -0
- data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +23940 -0
- data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +66 -0
- data/fixtures/vcr_cassettes/how-is-with-config-file.yml +23940 -0
- data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +81 -0
- data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +81 -0
- data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +247 -0
- data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +402 -0
- data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +325 -0
- data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +325 -0
- data/inq.gemspec +45 -0
- data/lib/inq.rb +63 -0
- data/lib/inq/cacheable.rb +71 -0
- data/lib/inq/cli.rb +135 -0
- data/lib/inq/config.rb +123 -0
- data/lib/inq/constants.rb +9 -0
- data/lib/inq/date_time_helpers.rb +48 -0
- data/lib/inq/exe.rb +66 -0
- data/lib/inq/frontmatter.rb +51 -0
- data/lib/inq/report.rb +140 -0
- data/lib/inq/report_collection.rb +113 -0
- data/lib/inq/sources.rb +11 -0
- data/lib/inq/sources/ci/appveyor.rb +87 -0
- data/lib/inq/sources/ci/travis.rb +159 -0
- data/lib/inq/sources/github.rb +57 -0
- data/lib/inq/sources/github/contributions.rb +204 -0
- data/lib/inq/sources/github/issue_fetcher.rb +148 -0
- data/lib/inq/sources/github/issues.rb +126 -0
- data/lib/inq/sources/github/pulls.rb +29 -0
- data/lib/inq/sources/github_helpers.rb +106 -0
- data/lib/inq/template.rb +9 -0
- data/lib/inq/templates/contributions_partial.html +1 -0
- data/lib/inq/templates/issues_or_pulls_partial.html +7 -0
- data/lib/inq/templates/new_contributors_partial.html +5 -0
- data/lib/inq/templates/report.html +19 -0
- data/lib/inq/templates/report_partial.html +12 -0
- data/lib/inq/text.rb +26 -0
- data/lib/inq/version.rb +6 -0
- 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
|
data/lib/inq/sources.rb
ADDED
@@ -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
|