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