how_is 8.0.0 → 9.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 +4 -4
- data/.gitignore +15 -15
- data/.rspec +4 -0
- data/.rspec-ignore-tags +2 -0
- data/.travis.yml +8 -4
- data/Gemfile +1 -0
- data/README.md +76 -76
- data/Rakefile +24 -0
- data/data/issues.plg +22 -22
- data/exe/how_is +30 -75
- data/how_is.gemspec +38 -37
- data/lib/how_is.rb +58 -56
- data/lib/how_is/analyzer.rb +170 -170
- data/lib/how_is/chart.rb +83 -83
- data/lib/how_is/cli.rb +90 -92
- data/lib/how_is/cli/parser.rb +76 -0
- data/lib/how_is/fetcher.rb +45 -45
- data/lib/how_is/pulse.rb +29 -29
- data/lib/how_is/report.rb +92 -92
- data/lib/how_is/report/html.rb +100 -100
- data/lib/how_is/report/json.rb +17 -17
- data/lib/how_is/report/pdf.rb +78 -78
- data/lib/how_is/version.rb +3 -3
- data/roadmap.markdown +49 -49
- metadata +21 -6
data/lib/how_is/chart.rb
CHANGED
@@ -1,83 +1,83 @@
|
|
1
|
-
class HowIs::Chart
|
2
|
-
# Generates the gnuplot script in data/issues.plg.
|
3
|
-
#
|
4
|
-
# Some configuration is available. Font locations are path to a TTF or other
|
5
|
-
# Gnuplot-readable font name.
|
6
|
-
#
|
7
|
-
# For example that could be '/Users/anne/Library/Fonts/InputMono-Medium.ttf'
|
8
|
-
# or just 'Helvetica'.
|
9
|
-
#
|
10
|
-
# @param font_location [String] Font for the chart
|
11
|
-
# @param font_size [Integer] Size of the chart text
|
12
|
-
# @param label_font_location [String] Font for labels
|
13
|
-
# @param label_font_size [Integer] Size of the label text
|
14
|
-
#
|
15
|
-
# @return void
|
16
|
-
def self.gnuplot(font_location: nil,
|
17
|
-
font_size: 16,
|
18
|
-
label_font_location: nil,
|
19
|
-
label_font_size: 10,
|
20
|
-
chartsize: '500,500',
|
21
|
-
data_file:,
|
22
|
-
png_file:)
|
23
|
-
default_font_location =
|
24
|
-
if Gem.win_platform?
|
25
|
-
'Arial'
|
26
|
-
else
|
27
|
-
'Helvetica'
|
28
|
-
end
|
29
|
-
|
30
|
-
font_location ||= default_font_location
|
31
|
-
label_font_location ||= font_location
|
32
|
-
|
33
|
-
cmd = %Q{
|
34
|
-
gnuplot -e "labelfont='#{label_font_location},#{label_font_size}'" \
|
35
|
-
-e "chartfont='#{font_location},#{font_size}'" \
|
36
|
-
-e "chartsize='#{chartsize}'" \
|
37
|
-
-e "data='#{data_file}'" \
|
38
|
-
-e "pngfile='#{png_file}'" \
|
39
|
-
-c data/issues.plg
|
40
|
-
}
|
41
|
-
puts cmd
|
42
|
-
IO.popen(cmd, 'w')
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.rotate(offset, filename)
|
46
|
-
if Gem.win_platform?
|
47
|
-
rotate_with_dotnet(filename, offset)
|
48
|
-
else
|
49
|
-
rotate_with_minimagick(filename, offset)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.rotate_with_dotnet(filename, offset)
|
54
|
-
ps_rotate_flip = {
|
55
|
-
90 => 'Rotate90FlipNone',
|
56
|
-
180 => 'Rotate180FlipNone',
|
57
|
-
270 => 'Rotate270FlipNone',
|
58
|
-
-90 => 'Rotate270FlipNone'
|
59
|
-
}[offset]
|
60
|
-
|
61
|
-
command = %Q{
|
62
|
-
$path = "#{filename}"
|
63
|
-
|
64
|
-
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");
|
65
|
-
$i = new-object System.Drawing.Bitmap $path
|
66
|
-
|
67
|
-
$i.RotateFlip("#{ps_rotate_flip}")
|
68
|
-
|
69
|
-
$i.Save($path,"png")
|
70
|
-
|
71
|
-
exit
|
72
|
-
}
|
73
|
-
|
74
|
-
IO.popen(["powershell", "-Command", command], 'w') { |io| }
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.rotate_with_minimagick(filename, offset)
|
78
|
-
require 'mini_magick'
|
79
|
-
image = MiniMagick::Image.new(filename) { |b| b.rotate offset.to_s }
|
80
|
-
image.format 'png'
|
81
|
-
image.write filename
|
82
|
-
end
|
83
|
-
end
|
1
|
+
class HowIs::Chart
|
2
|
+
# Generates the gnuplot script in data/issues.plg.
|
3
|
+
#
|
4
|
+
# Some configuration is available. Font locations are path to a TTF or other
|
5
|
+
# Gnuplot-readable font name.
|
6
|
+
#
|
7
|
+
# For example that could be '/Users/anne/Library/Fonts/InputMono-Medium.ttf'
|
8
|
+
# or just 'Helvetica'.
|
9
|
+
#
|
10
|
+
# @param font_location [String] Font for the chart
|
11
|
+
# @param font_size [Integer] Size of the chart text
|
12
|
+
# @param label_font_location [String] Font for labels
|
13
|
+
# @param label_font_size [Integer] Size of the label text
|
14
|
+
#
|
15
|
+
# @return void
|
16
|
+
def self.gnuplot(font_location: nil,
|
17
|
+
font_size: 16,
|
18
|
+
label_font_location: nil,
|
19
|
+
label_font_size: 10,
|
20
|
+
chartsize: '500,500',
|
21
|
+
data_file:,
|
22
|
+
png_file:)
|
23
|
+
default_font_location =
|
24
|
+
if Gem.win_platform?
|
25
|
+
'Arial'
|
26
|
+
else
|
27
|
+
'Helvetica'
|
28
|
+
end
|
29
|
+
|
30
|
+
font_location ||= default_font_location
|
31
|
+
label_font_location ||= font_location
|
32
|
+
|
33
|
+
cmd = %Q{
|
34
|
+
gnuplot -e "labelfont='#{label_font_location},#{label_font_size}'" \
|
35
|
+
-e "chartfont='#{font_location},#{font_size}'" \
|
36
|
+
-e "chartsize='#{chartsize}'" \
|
37
|
+
-e "data='#{data_file}'" \
|
38
|
+
-e "pngfile='#{png_file}'" \
|
39
|
+
-c data/issues.plg
|
40
|
+
}
|
41
|
+
puts cmd
|
42
|
+
IO.popen(cmd, 'w')
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.rotate(offset, filename)
|
46
|
+
if Gem.win_platform?
|
47
|
+
rotate_with_dotnet(filename, offset)
|
48
|
+
else
|
49
|
+
rotate_with_minimagick(filename, offset)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.rotate_with_dotnet(filename, offset)
|
54
|
+
ps_rotate_flip = {
|
55
|
+
90 => 'Rotate90FlipNone',
|
56
|
+
180 => 'Rotate180FlipNone',
|
57
|
+
270 => 'Rotate270FlipNone',
|
58
|
+
-90 => 'Rotate270FlipNone'
|
59
|
+
}[offset]
|
60
|
+
|
61
|
+
command = %Q{
|
62
|
+
$path = "#{filename}"
|
63
|
+
|
64
|
+
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");
|
65
|
+
$i = new-object System.Drawing.Bitmap $path
|
66
|
+
|
67
|
+
$i.RotateFlip("#{ps_rotate_flip}")
|
68
|
+
|
69
|
+
$i.Save($path,"png")
|
70
|
+
|
71
|
+
exit
|
72
|
+
}
|
73
|
+
|
74
|
+
IO.popen(["powershell", "-Command", command], 'w') { |io| }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.rotate_with_minimagick(filename, offset)
|
78
|
+
require 'mini_magick'
|
79
|
+
image = MiniMagick::Image.new(filename) { |b| b.rotate offset.to_s }
|
80
|
+
image.format 'png'
|
81
|
+
image.write filename
|
82
|
+
end
|
83
|
+
end
|
data/lib/how_is/cli.rb
CHANGED
@@ -1,92 +1,90 @@
|
|
1
|
-
require 'how_is'
|
2
|
-
require 'yaml'
|
3
|
-
require 'contracts'
|
4
|
-
require 'stringio'
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
config_file
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
str.puts
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
str.
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
1
|
+
require 'how_is'
|
2
|
+
require 'yaml'
|
3
|
+
require 'contracts'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
class HowIs::CLI
|
7
|
+
include Contracts::Core
|
8
|
+
|
9
|
+
DEFAULT_CONFIG_FILE = 'how_is.yml'
|
10
|
+
|
11
|
+
# Generates YAML frontmatter, as is used in Jekyll and other blog engines.
|
12
|
+
#
|
13
|
+
# E.g.,
|
14
|
+
# generate_frontmatter({'foo' => "bar %{baz}"}, {'baz' => "asdf"})
|
15
|
+
# => "---\nfoo: bar asdf\n"
|
16
|
+
Contract C::HashOf[C::Or[String, Symbol] => String],
|
17
|
+
C::HashOf[C::Or[String, Symbol] => C::Any] => String
|
18
|
+
def generate_frontmatter(frontmatter, report_data)
|
19
|
+
frontmatter = convert_keys(frontmatter, :to_s)
|
20
|
+
report_data = convert_keys(report_data, :to_sym)
|
21
|
+
|
22
|
+
frontmatter = frontmatter.map { |k, v|
|
23
|
+
v = v % report_data
|
24
|
+
|
25
|
+
[k, v]
|
26
|
+
}.to_h
|
27
|
+
|
28
|
+
YAML.dump(frontmatter)
|
29
|
+
end
|
30
|
+
|
31
|
+
def from_config_file(config_file = nil, **kwargs)
|
32
|
+
config_file ||= DEFAULT_CONFIG_FILE
|
33
|
+
|
34
|
+
from_config(YAML.load_file(config_file), **kwargs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def from_config(config,
|
38
|
+
github: nil,
|
39
|
+
report_class: nil)
|
40
|
+
report_class ||= HowIs::Report
|
41
|
+
|
42
|
+
date = Date.strptime(Time.now.to_i.to_s, '%s')
|
43
|
+
date_string = date.strftime('%Y-%m-%d')
|
44
|
+
friendly_date = date.strftime('%B %d, %y')
|
45
|
+
|
46
|
+
analysis = HowIs.generate_analysis(repository: config['repository'], github: github)
|
47
|
+
|
48
|
+
report_data = {
|
49
|
+
repository: config['repository'],
|
50
|
+
date: date,
|
51
|
+
friendly_date: friendly_date,
|
52
|
+
}
|
53
|
+
|
54
|
+
config['reports'].map do |format, report_config|
|
55
|
+
filename = report_config['filename'] % report_data
|
56
|
+
file = File.join(report_config['directory'], filename)
|
57
|
+
|
58
|
+
report = report_class.export(analysis, format)
|
59
|
+
|
60
|
+
result = build_report(report_config['frontmatter'], report_data, report)
|
61
|
+
|
62
|
+
File.open(file, 'w') do |f|
|
63
|
+
f.puts result
|
64
|
+
end
|
65
|
+
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_report(frontmatter, report_data, report)
|
71
|
+
str = StringIO.new
|
72
|
+
|
73
|
+
if frontmatter
|
74
|
+
str.puts generate_frontmatter(frontmatter, report_data)
|
75
|
+
str.puts "---"
|
76
|
+
str.puts
|
77
|
+
end
|
78
|
+
|
79
|
+
str.puts report
|
80
|
+
|
81
|
+
str.string
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
# convert_keys({'foo' => 'bar'}, :to_sym)
|
86
|
+
# => {:foo => 'bar'}
|
87
|
+
def convert_keys(data, method_name)
|
88
|
+
data.map {|k, v| [k.send(method_name), v]}.to_h
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "how_is"
|
4
|
+
require "how_is/cli"
|
5
|
+
require "slop"
|
6
|
+
|
7
|
+
class HowIs::CLI
|
8
|
+
DEFAULT_REPORT_FILE = "report.#{HowIs::DEFAULT_FORMAT}"
|
9
|
+
|
10
|
+
class OptionsError < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class Parser
|
14
|
+
attr_reader :opts
|
15
|
+
|
16
|
+
def call(argv)
|
17
|
+
opts = Slop::Options.new
|
18
|
+
opts.banner =
|
19
|
+
<<-EOF.gsub(/ *\| ?/, '')
|
20
|
+
| Usage: how_is REPOSITORY [--report REPORT_FILE]
|
21
|
+
| how_is --config CONFIG_FILE
|
22
|
+
|
|
23
|
+
| Where REPOSITORY is of the format <GitHub username or org>/<repository name>.
|
24
|
+
| CONFIG_FILE defaults to how_is.yml.
|
25
|
+
|
|
26
|
+
| E.g., if you wanted to check https://github.com/how-is/how_is,
|
27
|
+
| you'd run `how_is how-is/how_is`.
|
28
|
+
|
|
29
|
+
EOF
|
30
|
+
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator "Options:"
|
33
|
+
|
34
|
+
opts.bool "-h", "--help", "Print help text"
|
35
|
+
opts.string "--config", "YAML config file, used to generate a group of reports"
|
36
|
+
opts.string "--from", "JSON report file, used instead of fetching the data again"
|
37
|
+
opts.string "--report", "output file for the report (valid extensions: #{HowIs.supported_formats.join(', ')}; default: #{DEFAULT_REPORT_FILE})"
|
38
|
+
opts.string "-v", "--version", "prints the version"
|
39
|
+
|
40
|
+
parser = Slop::Parser.new(opts)
|
41
|
+
result = parser.parse(argv)
|
42
|
+
options = result.to_hash
|
43
|
+
arguments = result.arguments
|
44
|
+
|
45
|
+
options[:report] ||= DEFAULT_REPORT_FILE
|
46
|
+
|
47
|
+
# The following are only useful if true.
|
48
|
+
# Removing them here simplifies contracts and keyword args for other APIs.
|
49
|
+
options.delete(:config) unless options[:config]
|
50
|
+
options.delete(:help) unless options[:help]
|
51
|
+
options.delete(:version) unless options[:version]
|
52
|
+
|
53
|
+
unless HowIs.can_export_to?(options[:report])
|
54
|
+
raise OptionsError, "Invalid file: #{options[:report_file]}. Supported formats: #{HowIs.supported_formats.join(', ')}"
|
55
|
+
end
|
56
|
+
|
57
|
+
if options[:config]
|
58
|
+
# Nothing to do.
|
59
|
+
elsif options[:from]
|
60
|
+
# Opening this file here seems a bit messy, but it works.
|
61
|
+
options[:repository] = JSON.parse(open(options[:from_file]).read)['repository']
|
62
|
+
raise OptionsError, "Invalid JSON report file." unless options[:repository]
|
63
|
+
elsif argv.length >= 1
|
64
|
+
options[:repository] = argv.delete_at(0)
|
65
|
+
else
|
66
|
+
raise OptionsError, "No repository specified."
|
67
|
+
end
|
68
|
+
|
69
|
+
{
|
70
|
+
opts: opts,
|
71
|
+
options: options,
|
72
|
+
arguments: arguments,
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/how_is/fetcher.rb
CHANGED
@@ -1,45 +1,45 @@
|
|
1
|
-
require 'contracts'
|
2
|
-
require 'github_api'
|
3
|
-
|
4
|
-
##
|
5
|
-
# Fetches data from GitHub.
|
6
|
-
class HowIs::Fetcher
|
7
|
-
include Contracts::Core
|
8
|
-
|
9
|
-
##
|
10
|
-
# Standardized representation for fetcher results.
|
11
|
-
#
|
12
|
-
# Implemented as a class instead of passing around a Hash so that it can
|
13
|
-
# be more easily referenced by Contracts.
|
14
|
-
class Results < Struct.new(:repository, :issues, :pulls)
|
15
|
-
include Contracts::Core
|
16
|
-
|
17
|
-
Contract String, C::ArrayOf[Hash], C::ArrayOf[Hash] => nil
|
18
|
-
def initialize(repository, issues, pulls)
|
19
|
-
super(repository, issues, pulls)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Struct defines #to_h, but not #to_hash, so we alias them.
|
23
|
-
alias_method :to_hash, :to_h
|
24
|
-
end
|
25
|
-
|
26
|
-
|
27
|
-
Contract String, C::Or[C::RespondTo[:issues, :pulls], nil] => Results
|
28
|
-
def call(repository,
|
29
|
-
github = nil)
|
30
|
-
github ||= Github.new(auto_pagination: true)
|
31
|
-
user, repo = repository.split('/', 2)
|
32
|
-
issues = github.issues.list user: user, repo: repo
|
33
|
-
pulls = github.pulls.list user: user, repo: repo
|
34
|
-
|
35
|
-
Results.new(
|
36
|
-
repository,
|
37
|
-
obj_to_array_of_hashes(issues),
|
38
|
-
obj_to_array_of_hashes(pulls)
|
39
|
-
)
|
40
|
-
end
|
41
|
-
|
42
|
-
private def obj_to_array_of_hashes(object)
|
43
|
-
object.to_a.map(&:to_h)
|
44
|
-
end
|
45
|
-
end
|
1
|
+
require 'contracts'
|
2
|
+
require 'github_api'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Fetches data from GitHub.
|
6
|
+
class HowIs::Fetcher
|
7
|
+
include Contracts::Core
|
8
|
+
|
9
|
+
##
|
10
|
+
# Standardized representation for fetcher results.
|
11
|
+
#
|
12
|
+
# Implemented as a class instead of passing around a Hash so that it can
|
13
|
+
# be more easily referenced by Contracts.
|
14
|
+
class Results < Struct.new(:repository, :issues, :pulls)
|
15
|
+
include Contracts::Core
|
16
|
+
|
17
|
+
Contract String, C::ArrayOf[Hash], C::ArrayOf[Hash] => nil
|
18
|
+
def initialize(repository, issues, pulls)
|
19
|
+
super(repository, issues, pulls)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Struct defines #to_h, but not #to_hash, so we alias them.
|
23
|
+
alias_method :to_hash, :to_h
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
Contract String, C::Or[C::RespondTo[:issues, :pulls], nil] => Results
|
28
|
+
def call(repository,
|
29
|
+
github = nil)
|
30
|
+
github ||= Github.new(auto_pagination: true)
|
31
|
+
user, repo = repository.split('/', 2)
|
32
|
+
issues = github.issues.list user: user, repo: repo
|
33
|
+
pulls = github.pulls.list user: user, repo: repo
|
34
|
+
|
35
|
+
Results.new(
|
36
|
+
repository,
|
37
|
+
obj_to_array_of_hashes(issues),
|
38
|
+
obj_to_array_of_hashes(pulls)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
private def obj_to_array_of_hashes(object)
|
43
|
+
object.to_a.map(&:to_h)
|
44
|
+
end
|
45
|
+
end
|