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/pulse.rb
CHANGED
@@ -1,29 +1,29 @@
|
|
1
|
-
require 'tessellator/fetcher'
|
2
|
-
|
3
|
-
module HowIs
|
4
|
-
# This entire class is a monstrous hack, because GitHub doesn't provide a good
|
5
|
-
# API for Pulse.
|
6
|
-
class Pulse
|
7
|
-
def initialize(repository)
|
8
|
-
@repository = repository
|
9
|
-
@pulse_page_response = fetch_pulse!(repository)
|
10
|
-
end
|
11
|
-
|
12
|
-
def text_summary
|
13
|
-
raise NotImplementedError
|
14
|
-
end
|
15
|
-
|
16
|
-
def html_summary
|
17
|
-
@pulse_page_response.body
|
18
|
-
.split('<div class="section diffstat-summary">').last
|
19
|
-
.split('</div>').first
|
20
|
-
.gsub('<a href="/', '<a href="https://github.com/')
|
21
|
-
.strip
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
def fetch_pulse!(repository, period='monthly')
|
26
|
-
Tessellator::Fetcher.new.call('get', "https://github.com/#{repository}/pulse/#{period}")
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
1
|
+
require 'tessellator/fetcher'
|
2
|
+
|
3
|
+
module HowIs
|
4
|
+
# This entire class is a monstrous hack, because GitHub doesn't provide a good
|
5
|
+
# API for Pulse.
|
6
|
+
class Pulse
|
7
|
+
def initialize(repository)
|
8
|
+
@repository = repository
|
9
|
+
@pulse_page_response = fetch_pulse!(repository)
|
10
|
+
end
|
11
|
+
|
12
|
+
def text_summary
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
def html_summary
|
17
|
+
@pulse_page_response.body
|
18
|
+
.split('<div class="section diffstat-summary">').last
|
19
|
+
.split('</div>').first
|
20
|
+
.gsub('<a href="/', '<a href="https://github.com/')
|
21
|
+
.strip
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def fetch_pulse!(repository, period='monthly')
|
26
|
+
Tessellator::Fetcher.new.call('get', "https://github.com/#{repository}/pulse/#{period}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/how_is/report.rb
CHANGED
@@ -1,92 +1,92 @@
|
|
1
|
-
module HowIs
|
2
|
-
class UnsupportedExportFormat < StandardError
|
3
|
-
def initialize(format)
|
4
|
-
super("Unsupported export format: #{format}")
|
5
|
-
end
|
6
|
-
end
|
7
|
-
|
8
|
-
##
|
9
|
-
# Represents a completed report.
|
10
|
-
class BaseReport < Struct.new(:analysis)
|
11
|
-
def format
|
12
|
-
raise NotImplementedError
|
13
|
-
end
|
14
|
-
|
15
|
-
def github_pulse_summary
|
16
|
-
@pulse ||= HowIs::Pulse.new(analysis.repository)
|
17
|
-
@pulse.send("#{format}_summary")
|
18
|
-
end
|
19
|
-
|
20
|
-
def to_h
|
21
|
-
analysis.to_h
|
22
|
-
end
|
23
|
-
alias :to_hash :to_h
|
24
|
-
|
25
|
-
def to_json
|
26
|
-
to_h.to_json
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
def issue_or_pr_summary(type, type_label)
|
31
|
-
oldest_date_format = "%b %e, %Y"
|
32
|
-
a = analysis
|
33
|
-
|
34
|
-
number_of_type = a.send("number_of_#{type}s")
|
35
|
-
|
36
|
-
type_link = a.send("#{type}s_url")
|
37
|
-
oldest = a.send("oldest_#{type}")
|
38
|
-
|
39
|
-
"There are #{link("#{number_of_type} #{type_label}s open", type_link)}. " +
|
40
|
-
"The average #{type_label} age is #{a.send("average_#{type}_age")}, and the " +
|
41
|
-
"#{link("oldest", oldest['html_url'])} was opened on #{oldest['date'].strftime(oldest_date_format)}."
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
class Report
|
46
|
-
require 'how_is/report/pdf'
|
47
|
-
require 'how_is/report/json'
|
48
|
-
require 'how_is/report/html'
|
49
|
-
|
50
|
-
REPORT_BLOCK = proc do
|
51
|
-
title "How is #{analysis.repository}?"
|
52
|
-
|
53
|
-
text github_pulse_summary
|
54
|
-
|
55
|
-
header "Pull Requests"
|
56
|
-
text issue_or_pr_summary "pull", "pull request"
|
57
|
-
|
58
|
-
header "Issues"
|
59
|
-
text issue_or_pr_summary "issue", "issue"
|
60
|
-
|
61
|
-
header "Issues Per Label"
|
62
|
-
issues_per_label = analysis.issues_with_label.to_a.sort_by { |(k, v)| v['total'].to_i }.reverse
|
63
|
-
issues_per_label.map! do |label, hash|
|
64
|
-
[label, hash['total'], hash['link']]
|
65
|
-
end
|
66
|
-
issues_per_label << ["(No label)", analysis.issues_with_no_label['total'], nil]
|
67
|
-
horizontal_bar_graph issues_per_label
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.export!(analysis, file)
|
71
|
-
format = file.split('.').last
|
72
|
-
report = get_report_class(format).new(analysis)
|
73
|
-
|
74
|
-
report.export!(file, &REPORT_BLOCK)
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.export(analysis, format =
|
78
|
-
report = get_report_class(format).new(analysis)
|
79
|
-
|
80
|
-
report.export(&REPORT_BLOCK)
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
def self.get_report_class(format)
|
85
|
-
class_name = "#{format.capitalize}Report"
|
86
|
-
|
87
|
-
raise UnsupportedExportFormat, format unless HowIs.const_defined?(class_name)
|
88
|
-
|
89
|
-
HowIs.const_get(class_name)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
1
|
+
module HowIs
|
2
|
+
class UnsupportedExportFormat < StandardError
|
3
|
+
def initialize(format)
|
4
|
+
super("Unsupported export format: #{format}")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# Represents a completed report.
|
10
|
+
class BaseReport < Struct.new(:analysis)
|
11
|
+
def format
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def github_pulse_summary
|
16
|
+
@pulse ||= HowIs::Pulse.new(analysis.repository)
|
17
|
+
@pulse.send("#{format}_summary")
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
analysis.to_h
|
22
|
+
end
|
23
|
+
alias :to_hash :to_h
|
24
|
+
|
25
|
+
def to_json
|
26
|
+
to_h.to_json
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def issue_or_pr_summary(type, type_label)
|
31
|
+
oldest_date_format = "%b %e, %Y"
|
32
|
+
a = analysis
|
33
|
+
|
34
|
+
number_of_type = a.send("number_of_#{type}s")
|
35
|
+
|
36
|
+
type_link = a.send("#{type}s_url")
|
37
|
+
oldest = a.send("oldest_#{type}")
|
38
|
+
|
39
|
+
"There are #{link("#{number_of_type} #{type_label}s open", type_link)}. " +
|
40
|
+
"The average #{type_label} age is #{a.send("average_#{type}_age")}, and the " +
|
41
|
+
"#{link("oldest", oldest['html_url'])} was opened on #{oldest['date'].strftime(oldest_date_format)}."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Report
|
46
|
+
require 'how_is/report/pdf'
|
47
|
+
require 'how_is/report/json'
|
48
|
+
require 'how_is/report/html'
|
49
|
+
|
50
|
+
REPORT_BLOCK = proc do
|
51
|
+
title "How is #{analysis.repository}?"
|
52
|
+
|
53
|
+
text github_pulse_summary
|
54
|
+
|
55
|
+
header "Pull Requests"
|
56
|
+
text issue_or_pr_summary "pull", "pull request"
|
57
|
+
|
58
|
+
header "Issues"
|
59
|
+
text issue_or_pr_summary "issue", "issue"
|
60
|
+
|
61
|
+
header "Issues Per Label"
|
62
|
+
issues_per_label = analysis.issues_with_label.to_a.sort_by { |(k, v)| v['total'].to_i }.reverse
|
63
|
+
issues_per_label.map! do |label, hash|
|
64
|
+
[label, hash['total'], hash['link']]
|
65
|
+
end
|
66
|
+
issues_per_label << ["(No label)", analysis.issues_with_no_label['total'], nil]
|
67
|
+
horizontal_bar_graph issues_per_label
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.export!(analysis, file)
|
71
|
+
format = file.split('.').last
|
72
|
+
report = get_report_class(format).new(analysis)
|
73
|
+
|
74
|
+
report.export!(file, &REPORT_BLOCK)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.export(analysis, format = HowIs::DEFAULT_FORMAT)
|
78
|
+
report = get_report_class(format).new(analysis)
|
79
|
+
|
80
|
+
report.export(&REPORT_BLOCK)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def self.get_report_class(format)
|
85
|
+
class_name = "#{format.capitalize}Report"
|
86
|
+
|
87
|
+
raise UnsupportedExportFormat, format unless HowIs.const_defined?(class_name)
|
88
|
+
|
89
|
+
HowIs.const_get(class_name)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/how_is/report/html.rb
CHANGED
@@ -1,100 +1,100 @@
|
|
1
|
-
require 'cgi'
|
2
|
-
require 'how_is/pulse'
|
3
|
-
|
4
|
-
module HowIs
|
5
|
-
class HtmlReport < BaseReport
|
6
|
-
def format
|
7
|
-
:html
|
8
|
-
end
|
9
|
-
|
10
|
-
def title(_text)
|
11
|
-
@title = _text
|
12
|
-
@r += "<h1>#{_text}</h1>"
|
13
|
-
end
|
14
|
-
|
15
|
-
def header(_text)
|
16
|
-
@r += "<h2>#{_text}</h2>"
|
17
|
-
end
|
18
|
-
|
19
|
-
def link(_text, url)
|
20
|
-
%Q[<a href="#{url}">#{_text}</a>]
|
21
|
-
end
|
22
|
-
|
23
|
-
def monthly_summary
|
24
|
-
pulse.html_summary
|
25
|
-
end
|
26
|
-
|
27
|
-
def horizontal_bar_graph(data)
|
28
|
-
biggest = data.map { |x| x[1] }.max
|
29
|
-
get_percentage = ->(number_of_issues) { number_of_issues * 100 / biggest }
|
30
|
-
|
31
|
-
longest_label_length = data.map(&:first).map(&:length).max
|
32
|
-
label_width = "#{longest_label_length}ch"
|
33
|
-
|
34
|
-
@r += '<table class="horizontal-bar-graph">'
|
35
|
-
data.each do |row|
|
36
|
-
percentage = get_percentage.(row[1])
|
37
|
-
|
38
|
-
if row[2]
|
39
|
-
label_text = link(row[0], row[2])
|
40
|
-
else
|
41
|
-
label_text = row[1]
|
42
|
-
end
|
43
|
-
|
44
|
-
@r += <<-EOF
|
45
|
-
<tr>
|
46
|
-
<td style="width: #{label_width}">#{label_text}</td>
|
47
|
-
<td><span class="fill" style="width: #{percentage}%">#{row[1]}</span></td>
|
48
|
-
</tr>
|
49
|
-
EOF
|
50
|
-
end
|
51
|
-
@r += "</table>"
|
52
|
-
end
|
53
|
-
|
54
|
-
def text(_text)
|
55
|
-
@r += "<p>#{_text}</p>"
|
56
|
-
end
|
57
|
-
|
58
|
-
def export(&block)
|
59
|
-
@r = ''
|
60
|
-
instance_exec(&block)
|
61
|
-
end
|
62
|
-
|
63
|
-
def export!(file, &block)
|
64
|
-
report = export(&block)
|
65
|
-
|
66
|
-
File.open(file, 'w') do |f|
|
67
|
-
f.puts <<-EOF
|
68
|
-
<!DOCTYPE html>
|
69
|
-
<html>
|
70
|
-
<head>
|
71
|
-
<title>#{@title}</title>
|
72
|
-
<style>
|
73
|
-
body { font: sans-serif; }
|
74
|
-
main {
|
75
|
-
max-width: 600px;
|
76
|
-
max-width: 72ch;
|
77
|
-
margin: auto;
|
78
|
-
}
|
79
|
-
|
80
|
-
.horizontal-bar-graph {
|
81
|
-
position: relative;
|
82
|
-
width: 100%;
|
83
|
-
}
|
84
|
-
.horizontal-bar-graph .fill {
|
85
|
-
display: inline-block;
|
86
|
-
background: #CCC;
|
87
|
-
}
|
88
|
-
</style>
|
89
|
-
</head>
|
90
|
-
<body>
|
91
|
-
<main>
|
92
|
-
#{report}
|
93
|
-
</main>
|
94
|
-
</body>
|
95
|
-
</html>
|
96
|
-
EOF
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
1
|
+
require 'cgi'
|
2
|
+
require 'how_is/pulse'
|
3
|
+
|
4
|
+
module HowIs
|
5
|
+
class HtmlReport < BaseReport
|
6
|
+
def format
|
7
|
+
:html
|
8
|
+
end
|
9
|
+
|
10
|
+
def title(_text)
|
11
|
+
@title = _text
|
12
|
+
@r += "<h1>#{_text}</h1>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def header(_text)
|
16
|
+
@r += "<h2>#{_text}</h2>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def link(_text, url)
|
20
|
+
%Q[<a href="#{url}">#{_text}</a>]
|
21
|
+
end
|
22
|
+
|
23
|
+
def monthly_summary
|
24
|
+
pulse.html_summary
|
25
|
+
end
|
26
|
+
|
27
|
+
def horizontal_bar_graph(data)
|
28
|
+
biggest = data.map { |x| x[1] }.max
|
29
|
+
get_percentage = ->(number_of_issues) { number_of_issues * 100 / biggest }
|
30
|
+
|
31
|
+
longest_label_length = data.map(&:first).map(&:length).max
|
32
|
+
label_width = "#{longest_label_length}ch"
|
33
|
+
|
34
|
+
@r += '<table class="horizontal-bar-graph">'
|
35
|
+
data.each do |row|
|
36
|
+
percentage = get_percentage.(row[1])
|
37
|
+
|
38
|
+
if row[2]
|
39
|
+
label_text = link(row[0], row[2])
|
40
|
+
else
|
41
|
+
label_text = row[1]
|
42
|
+
end
|
43
|
+
|
44
|
+
@r += <<-EOF
|
45
|
+
<tr>
|
46
|
+
<td style="width: #{label_width}">#{label_text}</td>
|
47
|
+
<td><span class="fill" style="width: #{percentage}%">#{row[1]}</span></td>
|
48
|
+
</tr>
|
49
|
+
EOF
|
50
|
+
end
|
51
|
+
@r += "</table>"
|
52
|
+
end
|
53
|
+
|
54
|
+
def text(_text)
|
55
|
+
@r += "<p>#{_text}</p>"
|
56
|
+
end
|
57
|
+
|
58
|
+
def export(&block)
|
59
|
+
@r = ''
|
60
|
+
instance_exec(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def export!(file, &block)
|
64
|
+
report = export(&block)
|
65
|
+
|
66
|
+
File.open(file, 'w') do |f|
|
67
|
+
f.puts <<-EOF
|
68
|
+
<!DOCTYPE html>
|
69
|
+
<html>
|
70
|
+
<head>
|
71
|
+
<title>#{@title}</title>
|
72
|
+
<style>
|
73
|
+
body { font: sans-serif; }
|
74
|
+
main {
|
75
|
+
max-width: 600px;
|
76
|
+
max-width: 72ch;
|
77
|
+
margin: auto;
|
78
|
+
}
|
79
|
+
|
80
|
+
.horizontal-bar-graph {
|
81
|
+
position: relative;
|
82
|
+
width: 100%;
|
83
|
+
}
|
84
|
+
.horizontal-bar-graph .fill {
|
85
|
+
display: inline-block;
|
86
|
+
background: #CCC;
|
87
|
+
}
|
88
|
+
</style>
|
89
|
+
</head>
|
90
|
+
<body>
|
91
|
+
<main>
|
92
|
+
#{report}
|
93
|
+
</main>
|
94
|
+
</body>
|
95
|
+
</html>
|
96
|
+
EOF
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/how_is/report/json.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
|
-
module HowIs
|
2
|
-
class JsonReport < BaseReport
|
3
|
-
def format
|
4
|
-
:json
|
5
|
-
end
|
6
|
-
|
7
|
-
def export(&block)
|
8
|
-
to_json
|
9
|
-
end
|
10
|
-
|
11
|
-
def export!(file, &block)
|
12
|
-
File.open(file, 'w') do |f|
|
13
|
-
f.write export
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
1
|
+
module HowIs
|
2
|
+
class JsonReport < BaseReport
|
3
|
+
def format
|
4
|
+
:json
|
5
|
+
end
|
6
|
+
|
7
|
+
def export(&block)
|
8
|
+
to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def export!(file, &block)
|
12
|
+
File.open(file, 'w') do |f|
|
13
|
+
f.write export
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|