how_is 24.0.0 → 25.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github_changelog_generator +0 -1
- data/.rubocop.yml +37 -12
- data/.travis.yml +6 -3
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +8 -4
- data/ISSUES.md +30 -54
- data/README.md +16 -91
- data/Rakefile +3 -31
- data/bin/prerelease-generate-changelog +1 -1
- data/bin/setup +0 -0
- data/build-debug.rb +20 -0
- data/exe/how_is +25 -22
- data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +334 -1
- data/fixtures/vcr_cassettes/how-is-example-repository.yml +350 -1
- data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +15234 -1
- data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +2694 -1
- data/fixtures/vcr_cassettes/how-is-with-config-file.yml +15234 -1
- data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +70 -1
- data/how_is.gemspec +12 -6
- data/lib/how_is/cacheable.rb +71 -0
- data/lib/how_is/cli.rb +121 -124
- data/lib/how_is/config.rb +123 -0
- data/lib/how_is/constants.rb +9 -0
- data/lib/how_is/date_time_helpers.rb +48 -0
- data/lib/how_is/frontmatter.rb +14 -9
- data/lib/how_is/report.rb +86 -58
- data/lib/how_is/report_collection.rb +113 -0
- data/lib/how_is/sources/ci/appveyor.rb +88 -0
- data/lib/how_is/sources/ci/travis.rb +159 -0
- data/lib/how_is/sources/github/contributions.rb +169 -128
- data/lib/how_is/sources/github/issue_fetcher.rb +148 -0
- data/lib/how_is/sources/github/issues.rb +86 -235
- data/lib/how_is/sources/github/pulls.rb +19 -18
- data/lib/how_is/sources/github.rb +40 -18
- data/lib/how_is/sources/github_helpers.rb +8 -91
- data/lib/how_is/sources.rb +2 -0
- data/lib/how_is/template.rb +9 -0
- data/lib/how_is/templates/contributions_partial.html +1 -0
- data/lib/how_is/templates/{issues_or_pulls_partial.html_template → issues_or_pulls_partial.html} +0 -0
- data/lib/how_is/templates/new_contributors_partial.html +5 -0
- data/lib/how_is/templates/{report.html_template → report.html} +0 -8
- data/lib/how_is/templates/{report_partial.html_template → report_partial.html} +3 -3
- data/lib/how_is/text.rb +26 -0
- data/lib/how_is/version.rb +2 -1
- data/lib/how_is.rb +33 -60
- metadata +28 -47
- data/.hound.yml +0 -2
- data/.rubocop_todo.yml +0 -21
- data/lib/how_is/sources/travis.rb +0 -37
- data/roadmap.markdown +0 -82
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "how_is/text"
|
5
|
+
|
6
|
+
module HowIs
|
7
|
+
HOME_CONFIG = File.join(Dir.home, ".config", "how_is", "config.yml")
|
8
|
+
|
9
|
+
# Usage:
|
10
|
+
# HowIs::Config
|
11
|
+
# .load_site_configs("/path/to/config1.yml", "/path/to/config2.yml")
|
12
|
+
# .load_file("./repo-config.yml")
|
13
|
+
# Or:
|
14
|
+
# HowIs::Config
|
15
|
+
# .load_defaults
|
16
|
+
# .load_file("./repo-config.yml")
|
17
|
+
# Or:
|
18
|
+
# HowIs::Config
|
19
|
+
# .load_defaults
|
20
|
+
# .load({"repository" => "how-is/example-repository"})
|
21
|
+
class Config < Hash
|
22
|
+
attr_reader :site_configs
|
23
|
+
|
24
|
+
# If the +HOWIS_USE_ENV+ environment variable is set, load config from
|
25
|
+
# the environment.
|
26
|
+
#
|
27
|
+
# Otherwise, load the the default config file.
|
28
|
+
#
|
29
|
+
# @return [Hash] A Hash representation of the config.
|
30
|
+
def load_defaults
|
31
|
+
if ENV["HOWIS_USE_ENV"] == "true"
|
32
|
+
load_env
|
33
|
+
else
|
34
|
+
load_site_configs(HOME_CONFIG)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
super()
|
40
|
+
@site_configs = []
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load the config files as specified via +files+.
|
44
|
+
#
|
45
|
+
# @param files [Array<String>] The path(s) for config files.
|
46
|
+
# @return [Config] The config hash. (+self+)
|
47
|
+
def load_site_configs(*files)
|
48
|
+
# Allows both:
|
49
|
+
# load_site_configs('foo', 'bar')
|
50
|
+
# load_site_configs(['foo', bar'])
|
51
|
+
# but not:
|
52
|
+
# load_site_configs(['foo'], 'bar')
|
53
|
+
files = files[0] if files.length == 1 && files[0].is_a?(Array)
|
54
|
+
|
55
|
+
load_files(*files)
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO: See if this can be consolidated with load_site_configs.
|
59
|
+
def load_files(*file_paths)
|
60
|
+
files = (site_configs + file_paths).map { |f| Pathname.new(f) }
|
61
|
+
|
62
|
+
# Keep only files that exist.
|
63
|
+
files.select!(&:file?)
|
64
|
+
|
65
|
+
# Load the YAML files into Hashes.
|
66
|
+
configs = files.map { |file| YAML.safe_load(file.read) }
|
67
|
+
|
68
|
+
# Apply configs.
|
69
|
+
load(*configs)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Take a collection of config hashes and cascade them, meaning values
|
73
|
+
# in later ones override values in earlier ones.
|
74
|
+
#
|
75
|
+
# E.g., this results in +{'a'=>'x', 'c'=>'d'}+:
|
76
|
+
# load({'a'=>'b'}, {'c'=>'d'}, {'a'=>'x'})
|
77
|
+
#
|
78
|
+
# And this results in +{'a'=>['b', 'c']}+:
|
79
|
+
# load({'a'=>['b']}, {'a'=>['c']})
|
80
|
+
#
|
81
|
+
# @param [Array<Hash>] The configuration hashes.
|
82
|
+
# @return [Config] The final configuration value.
|
83
|
+
def load(*configs)
|
84
|
+
configs.each do |config|
|
85
|
+
config.each do |k, v|
|
86
|
+
if self[k] && self[k].is_a?(Array)
|
87
|
+
self[k] += v
|
88
|
+
else
|
89
|
+
self[k] = v
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Load config info from environment variables.
|
98
|
+
#
|
99
|
+
# Supported environment variables:
|
100
|
+
# - HOWIS_GITHUB_TOKEN: a GitHub authentication token.
|
101
|
+
# - HOWIS_GITHUB_USERNAME: the GitHub username corresponding to the token.
|
102
|
+
#
|
103
|
+
# @return [Config] The resulting configuration.
|
104
|
+
def load_env
|
105
|
+
HowIs::Text.puts "Using configuration from environment variables."
|
106
|
+
|
107
|
+
gh_token = ENV["HOWIS_GITHUB_TOKEN"]
|
108
|
+
gh_username = ENV["HOWIS_GITHUB_USERNAME"]
|
109
|
+
|
110
|
+
raise "HOWIS_GITHUB_TOKEN environment variable is not set" \
|
111
|
+
unless gh_token
|
112
|
+
raise "HOWIS_GITHUB_USERNAME environment variable is not set" \
|
113
|
+
unless gh_username
|
114
|
+
|
115
|
+
load({
|
116
|
+
"sources/github" => {
|
117
|
+
"username" => gh_username,
|
118
|
+
"token" => gh_token,
|
119
|
+
},
|
120
|
+
})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HowIs
|
4
|
+
# The file name used for a report if one isn't specified.
|
5
|
+
DEFAULT_REPORT_FILE = "report.html"
|
6
|
+
|
7
|
+
# Used by things making HTTP requests.
|
8
|
+
USER_AGENT = "how_is/#{HowIs::VERSION} (https://github.com/how-is/how_is/)"
|
9
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "how_is/version"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
module HowIs
|
7
|
+
##
|
8
|
+
# Various helper functions for working with DateTime objects.
|
9
|
+
module DateTimeHelpers
|
10
|
+
# Check if +left+ is less than or equal to +right+, where both are string
|
11
|
+
# representations of a date.
|
12
|
+
#
|
13
|
+
# @param left [String] A string representation of a date.
|
14
|
+
# @param right [String] A string representation of a date.
|
15
|
+
# @return [Boolean] True if +left+ is less-than-or-equal to +right+,
|
16
|
+
# otherwise false.
|
17
|
+
def date_le(left, right)
|
18
|
+
left = str_to_dt(left)
|
19
|
+
right = str_to_dt(right)
|
20
|
+
|
21
|
+
left <= right
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if +left+ is greater than or equal to +right+, where both are string
|
25
|
+
# representations of a date.
|
26
|
+
#
|
27
|
+
# @param left [String] A string representation of a date.
|
28
|
+
# @param right [String] A string representation of a date.
|
29
|
+
# @return [Boolean] True if +left+ is greater-than-or-equal to +right+,
|
30
|
+
# otherwise false.
|
31
|
+
def date_ge(left, right)
|
32
|
+
left = str_to_dt(left)
|
33
|
+
right = str_to_dt(right)
|
34
|
+
|
35
|
+
left >= right
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Converts a +String+ representation of a date to a +DateTime+.
|
41
|
+
#
|
42
|
+
# @param str [String] A date.
|
43
|
+
# @return [DateTime] A DateTime representation of +str+.
|
44
|
+
def str_to_dt(str)
|
45
|
+
DateTime.parse(str)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/how_is/frontmatter.rb
CHANGED
@@ -1,14 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "how_is/version"
|
4
|
+
require "okay/warning_helpers"
|
4
5
|
|
5
6
|
module HowIs
|
7
|
+
##
|
8
|
+
# Module for generating YAML frontmatter, as used by Jekyll and other
|
9
|
+
# blog engines.
|
6
10
|
module Frontmatter
|
11
|
+
extend Okay::WarningHelpers
|
12
|
+
|
7
13
|
# Generates YAML frontmatter, as is used in Jekyll and other blog engines.
|
8
14
|
#
|
9
15
|
# E.g.,
|
10
16
|
# generate_frontmatter({'foo' => "bar %{baz}"}, {'baz' => "asdf"})
|
11
17
|
# => "---\nfoo: bar asdf\n"
|
18
|
+
#
|
19
|
+
# @param frontmatter [Hash] Frontmatter for the report.
|
20
|
+
# @param report_data [Hash] The report data itself.
|
21
|
+
# @return [String] A YAML dump of the generated frontmatter.
|
12
22
|
def self.generate(frontmatter, report_data)
|
13
23
|
return "" if frontmatter.nil?
|
14
24
|
|
@@ -29,18 +39,13 @@ module HowIs
|
|
29
39
|
# @example
|
30
40
|
# convert_keys({'foo' => 'bar'}, :to_sym)
|
31
41
|
# # => {:foo => 'bar'}
|
42
|
+
# @param data [Hash] The input hash.
|
43
|
+
# @param method_name [Symbol] The method name used to convert keys.
|
44
|
+
# (E.g. :to_s, :to_sym, etc.)
|
45
|
+
# @return [Hash] The converted result.
|
32
46
|
def self.convert_keys(data, method_name)
|
33
47
|
data.map { |k, v| [k.send(method_name), v] }.to_h
|
34
48
|
end
|
35
49
|
private_class_method :convert_keys
|
36
|
-
|
37
|
-
def self.silence_warnings(&_block)
|
38
|
-
old_verbose = $VERBOSE
|
39
|
-
$VERBOSE = nil # Disable warnings entirely.
|
40
|
-
yield
|
41
|
-
ensure
|
42
|
-
$VERBOSE = old_verbose
|
43
|
-
end
|
44
|
-
private_class_method :silence_warnings
|
45
50
|
end
|
46
51
|
end
|
data/lib/how_is/report.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "how_is/frontmatter"
|
4
|
+
require "how_is/cacheable"
|
4
5
|
require "how_is/sources/github/contributions"
|
5
6
|
require "how_is/sources/github/issues"
|
6
7
|
require "how_is/sources/github/pulls"
|
7
|
-
require "how_is/sources/travis"
|
8
|
+
require "how_is/sources/ci/travis"
|
9
|
+
require "how_is/sources/ci/appveyor"
|
10
|
+
require "how_is/template"
|
11
|
+
require "json"
|
8
12
|
|
9
13
|
module HowIs
|
14
|
+
##
|
15
|
+
# Class for generating a HowIs report.
|
10
16
|
class Report
|
11
|
-
def initialize(
|
12
|
-
@
|
17
|
+
def initialize(config, end_date)
|
18
|
+
@config = config
|
19
|
+
@repository = config["repository"]
|
13
20
|
|
14
21
|
# NOTE: Use DateTime because it defaults to UTC and that's less gross
|
15
22
|
# than trying to get Date to use UTC.
|
@@ -19,80 +26,50 @@ module HowIs
|
|
19
26
|
#
|
20
27
|
# (I'm also guessing/hoping that GitHub's URLs use UTC.)
|
21
28
|
end_dt = DateTime.strptime(end_date, "%Y-%m-%d")
|
22
|
-
|
23
|
-
d = end_dt.day
|
24
|
-
m = end_dt.month
|
25
|
-
y = end_dt.year
|
26
|
-
start_year = y
|
27
|
-
start_month = m - 1
|
28
|
-
if start_month <= 0
|
29
|
-
start_month = 12 - start_month
|
30
|
-
start_year -= 1
|
31
|
-
end
|
32
|
-
start_dt = DateTime.new(start_year, start_month, d)
|
29
|
+
start_dt = start_dt_from_end_dt(end_dt)
|
33
30
|
|
34
31
|
@end_date = end_dt.strftime("%Y-%m-%d")
|
35
32
|
@start_date = start_dt.strftime("%Y-%m-%d")
|
36
|
-
|
37
|
-
@gh_contributions = HowIs::Sources::Github::Contributions.new(repository, @start_date, @end_date)
|
38
|
-
@gh_issues = HowIs::Sources::Github::Issues.new(repository, @start_date, @end_date)
|
39
|
-
@gh_pulls = HowIs::Sources::Github::Pulls.new(repository, @start_date, @end_date)
|
40
|
-
@travis = HowIs::Sources::Travis.new(repository, @start_date, @end_date)
|
41
33
|
end
|
42
34
|
|
43
|
-
def
|
44
|
-
@
|
45
|
-
|
46
|
-
repository: @repository,
|
47
|
-
|
48
|
-
contributions_summary: @gh_contributions.to_html,
|
49
|
-
issues_summary: @gh_issues.to_html,
|
50
|
-
pulls_summary: @gh_pulls.to_html,
|
51
|
-
issues_per_label: @gh_issues.issues_per_label_html,
|
52
|
-
|
53
|
-
issues: @gh_issues.to_a,
|
54
|
-
pulls: @gh_issues.to_a,
|
55
|
-
|
56
|
-
average_issue_age: @gh_issues.average_age,
|
57
|
-
average_pull_age: @gh_pulls.average_age,
|
58
|
-
|
59
|
-
oldest_issue_link: @gh_issues.oldest["url"],
|
60
|
-
oldest_issue_date: @gh_issues.oldest["createdAt"],
|
35
|
+
def cache
|
36
|
+
@cache ||= Cacheable.new(@config, @start_date, @end_date)
|
37
|
+
end
|
61
38
|
|
62
|
-
|
63
|
-
|
39
|
+
def contributions
|
40
|
+
@gh_contributions ||= HowIs::Sources::Github::Contributions.new(@config, @start_date, @end_date, cache)
|
41
|
+
end
|
64
42
|
|
65
|
-
|
66
|
-
|
43
|
+
def issues
|
44
|
+
@gh_issues ||= HowIs::Sources::Github::Issues.new(@config, @start_date, @end_date, cache)
|
45
|
+
end
|
67
46
|
|
68
|
-
|
69
|
-
|
47
|
+
def pulls
|
48
|
+
@gh_pulls ||= HowIs::Sources::Github::Pulls.new(@config, @start_date, @end_date, cache)
|
49
|
+
end
|
70
50
|
|
71
|
-
|
51
|
+
def travis
|
52
|
+
@travis ||= HowIs::Sources::CI::Travis.new(@config, @start_date, @end_date, cache)
|
53
|
+
end
|
72
54
|
|
73
|
-
|
74
|
-
|
55
|
+
def appveyor
|
56
|
+
@appveyor ||= HowIs::Sources::CI::Appveyor.new(@config, @start_date, @end_date, cache)
|
57
|
+
end
|
75
58
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
""
|
81
|
-
end
|
59
|
+
def to_h(frontmatter_data = nil)
|
60
|
+
@report_hash ||= report_hash
|
61
|
+
frontmatter = HowIs::Frontmatter.generate(frontmatter_data, @report_hash)
|
82
62
|
|
83
63
|
@report_hash.merge(frontmatter: frontmatter)
|
84
64
|
end
|
85
65
|
|
86
66
|
def to_html_partial(frontmatter = nil)
|
87
|
-
|
88
|
-
|
89
|
-
Kernel.format(HowIs.template("report_partial.html_template"), template_data)
|
67
|
+
HowIs::Template.apply("report_partial.html", to_h(frontmatter))
|
90
68
|
end
|
91
69
|
|
92
70
|
def to_html(frontmatter = nil)
|
93
71
|
template_data = to_h(frontmatter).merge({report: to_html_partial})
|
94
|
-
|
95
|
-
Kernel.format(HowIs.template("report.html_template"), template_data)
|
72
|
+
HowIs::Template.apply("report.html", template_data)
|
96
73
|
end
|
97
74
|
|
98
75
|
def to_json(frontmatter = nil)
|
@@ -104,9 +81,60 @@ module HowIs
|
|
104
81
|
end
|
105
82
|
|
106
83
|
def to_format_for(filename)
|
107
|
-
format =
|
84
|
+
format = File.extname(filename)[1..-1]
|
108
85
|
send("to_#{format}")
|
109
86
|
end
|
110
87
|
private :to_format_for
|
88
|
+
|
89
|
+
def start_dt_from_end_dt(end_dt)
|
90
|
+
d = end_dt.day
|
91
|
+
m = end_dt.month
|
92
|
+
y = end_dt.year
|
93
|
+
start_year = y
|
94
|
+
start_month = m - 1
|
95
|
+
if start_month <= 0
|
96
|
+
start_month = 12 - start_month
|
97
|
+
start_year -= 1
|
98
|
+
end
|
99
|
+
|
100
|
+
DateTime.new(start_year, start_month, d)
|
101
|
+
end
|
102
|
+
|
103
|
+
# rubocop:disable Metrics/AbcSize
|
104
|
+
def report_hash
|
105
|
+
{
|
106
|
+
title: "How is #{@repository}?",
|
107
|
+
repository: @repository,
|
108
|
+
|
109
|
+
contributions_summary: contributions.to_html,
|
110
|
+
new_contributors: contributions.new_contributors_html,
|
111
|
+
issues_summary: issues.to_html,
|
112
|
+
pulls_summary: pulls.to_html,
|
113
|
+
|
114
|
+
issues: issues.to_a,
|
115
|
+
pulls: issues.to_a,
|
116
|
+
|
117
|
+
average_issue_age: issues.average_age,
|
118
|
+
average_pull_age: pulls.average_age,
|
119
|
+
|
120
|
+
oldest_issue_link: issues.oldest["url"],
|
121
|
+
oldest_issue_date: issues.oldest["createdAt"],
|
122
|
+
|
123
|
+
newest_issue_link: issues.newest["url"],
|
124
|
+
newest_issue_date: issues.newest["createdAt"],
|
125
|
+
|
126
|
+
newest_pull_link: pulls.newest["url"],
|
127
|
+
newest_pull_date: pulls.newest["createdAt"],
|
128
|
+
|
129
|
+
oldest_pull_link: pulls.oldest["url"],
|
130
|
+
oldest_pull_date: pulls.oldest["createdAt"],
|
131
|
+
|
132
|
+
travis_builds: travis.builds,
|
133
|
+
appveyor_builds: appveyor.builds,
|
134
|
+
|
135
|
+
date: @end_date,
|
136
|
+
}
|
137
|
+
end
|
138
|
+
# rubocop:enable Metrics/AbcSize
|
111
139
|
end
|
112
140
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "how_is/report"
|
4
|
+
require "okay/warning_helpers"
|
5
|
+
|
6
|
+
module HowIs
|
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,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "okay/default"
|
4
|
+
require "okay/http"
|
5
|
+
require "how_is/constants"
|
6
|
+
require "how_is/sources"
|
7
|
+
require "how_is/sources/github/contributions"
|
8
|
+
require "how_is/text"
|
9
|
+
|
10
|
+
module HowIs
|
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 HowIs::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 =
|
33
|
+
HowIs::Sources::GitHub::Contributions.new(@config, nil, nil)
|
34
|
+
|
35
|
+
@default_branch = contributions.default_branch
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fetches builds for the default branch.
|
39
|
+
#
|
40
|
+
# @return [Hash] Builds for the default branch.
|
41
|
+
def builds
|
42
|
+
@builds ||=
|
43
|
+
fetch_builds["builds"] \
|
44
|
+
.map(&method(:normalize_build)) \
|
45
|
+
.select(&method(:in_date_range?))
|
46
|
+
rescue Net::HTTPServerException
|
47
|
+
# It's not elegant, but it works™.
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def in_date_range?(build)
|
54
|
+
build["started_at"] >= @start_date &&
|
55
|
+
build["started_at"] <= @end_date
|
56
|
+
end
|
57
|
+
|
58
|
+
def normalize_build(build)
|
59
|
+
build["started_at"] = DateTime.parse(build["created"])
|
60
|
+
build["html_url"] = "https://ci.appveyor.com/project/#{@repository}/build/#{build['buildNumber']}"
|
61
|
+
build
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns API result of /api/projects/:repository.
|
65
|
+
# FIXME: This doesn't limit results based on the date range.
|
66
|
+
#
|
67
|
+
# @return [Hash] API results.
|
68
|
+
def fetch_builds
|
69
|
+
@cache.cached("appveyor_builds") do
|
70
|
+
HowIs::Text.print "Fetching Appveyor build data."
|
71
|
+
|
72
|
+
ret = Okay::HTTP.get(
|
73
|
+
"https://ci.appveyor.com/api/projects/#{@repository}/history",
|
74
|
+
parameters: {"recordsNumber" => "100"},
|
75
|
+
headers: {
|
76
|
+
"Accept" => "application/json",
|
77
|
+
"User-Agent" => HowIs::USER_AGENT,
|
78
|
+
}
|
79
|
+
).or_raise!.from_json
|
80
|
+
|
81
|
+
HowIs::Text.puts
|
82
|
+
ret
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|