hubtime 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +56 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/bin/hubtime +199 -0
- data/hubtime.gemspec +28 -0
- data/lib/hubtime.rb +16 -0
- data/lib/hubtime/activity.rb +494 -0
- data/lib/hubtime/cacher.rb +48 -0
- data/lib/hubtime/charts/graph.erb +92 -0
- data/lib/hubtime/charts/impact.erb +90 -0
- data/lib/hubtime/charts/pie.erb +51 -0
- data/lib/hubtime/commit.rb +42 -0
- data/lib/hubtime/github.rb +67 -0
- data/lib/hubtime/hub_config.rb +122 -0
- data/lib/hubtime/repo.rb +174 -0
- data/lib/hubtime/version.rb +3 -0
- data/readme/graph.png +0 -0
- data/readme/impact.png +0 -0
- data/readme/pie.png +0 -0
- data/readme/stacked.png +0 -0
- metadata +196 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module Hubtime
|
4
|
+
class Cacher
|
5
|
+
def self.clear(type)
|
6
|
+
dir = nil
|
7
|
+
case type
|
8
|
+
when "all"
|
9
|
+
dir = File.join(File.expand_path("."), "data")
|
10
|
+
when "cache", "charts"
|
11
|
+
dir = File.join(File.expand_path("."), "data", type)
|
12
|
+
else
|
13
|
+
dir = File.join(File.expand_path("."), "data", "cache", type)
|
14
|
+
end
|
15
|
+
|
16
|
+
raise "Unknown clear type" unless dir
|
17
|
+
FileUtils.rm_rf(dir, :secure => true) if File.exist?(dir)
|
18
|
+
end
|
19
|
+
def initialize(directory)
|
20
|
+
root = File.join(File.expand_path("."), "data", "cache")
|
21
|
+
@directory = File.join(root, directory)
|
22
|
+
FileUtils.mkdir_p(@directory)
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(key)
|
26
|
+
file_name = File.join(@directory, sanitized_file_name_from(key))
|
27
|
+
return nil unless File.exist?(file_name)
|
28
|
+
YAML.load(File.read(file_name))
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(key, value)
|
32
|
+
file_name = File.join(@directory, sanitized_file_name_from(key))
|
33
|
+
directory = File.dirname(file_name)
|
34
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
35
|
+
File.open(file_name, 'w') {|f| f.write(YAML.dump(value)) }
|
36
|
+
value
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def sanitized_file_name_from(file_name)
|
42
|
+
parts = file_name.to_s.split('.')
|
43
|
+
file_extension = '.' + parts.pop if parts.size > 1
|
44
|
+
base = parts.join('.').gsub(/[^\w\-\/]+/, '_') + file_extension.to_s
|
45
|
+
"#{base}.yml"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
<%
|
2
|
+
data_type = "commits" if data_type == "count"
|
3
|
+
other_type = "commits" if other_type == "count"
|
4
|
+
title = "#{data_type.titleize} by Week: #{username}"
|
5
|
+
%>
|
6
|
+
|
7
|
+
<html>
|
8
|
+
<head>
|
9
|
+
<title><%= title %></title>
|
10
|
+
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
11
|
+
<script src="http://code.highcharts.com/highcharts.js"></script>
|
12
|
+
<script src="http://code.highcharts.com/modules/exporting.js"></script>
|
13
|
+
</head>
|
14
|
+
<body>
|
15
|
+
<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>
|
16
|
+
|
17
|
+
<script type="text/javascript">
|
18
|
+
//<![CDATA[
|
19
|
+
var chart = new Highcharts.Chart({
|
20
|
+
chart: {
|
21
|
+
renderTo: 'container',
|
22
|
+
},
|
23
|
+
plotOptions: {
|
24
|
+
series: {
|
25
|
+
shadow: false,
|
26
|
+
lineWidth: 1,
|
27
|
+
marker: {
|
28
|
+
enabled: false
|
29
|
+
}
|
30
|
+
},
|
31
|
+
area: {
|
32
|
+
stacking: 'normal'
|
33
|
+
}
|
34
|
+
},
|
35
|
+
title: {
|
36
|
+
text: '<%= title %>'
|
37
|
+
},
|
38
|
+
xAxis: {
|
39
|
+
categories: <%= labels.to_json %>
|
40
|
+
},
|
41
|
+
yAxis: [
|
42
|
+
{ // data
|
43
|
+
title: {
|
44
|
+
text: ''
|
45
|
+
},
|
46
|
+
min: 0
|
47
|
+
},
|
48
|
+
{ // other
|
49
|
+
title: {
|
50
|
+
text: ''
|
51
|
+
},
|
52
|
+
labels: {
|
53
|
+
style: {
|
54
|
+
color: '#5EAAE4'
|
55
|
+
}
|
56
|
+
},
|
57
|
+
opposite: true,
|
58
|
+
min: 0
|
59
|
+
}
|
60
|
+
],
|
61
|
+
tooltip: {
|
62
|
+
formatter: function() {
|
63
|
+
return '' + this.series.name + ': ' + this.y.toString();
|
64
|
+
}
|
65
|
+
},
|
66
|
+
credits: {
|
67
|
+
enabled: false
|
68
|
+
},
|
69
|
+
series: [
|
70
|
+
<% data.each do |key, array| %>
|
71
|
+
{
|
72
|
+
name: '<%= key.include?('/') ? key : key.titleize %>',
|
73
|
+
<%= "color: '#F17F49'," if data.size == 1 %>
|
74
|
+
|
75
|
+
type: 'area',
|
76
|
+
data: <%= array.to_json %>
|
77
|
+
},
|
78
|
+
<% end %>
|
79
|
+
{
|
80
|
+
name: '<%= other_type.titleize %>',
|
81
|
+
color: '#5EAAE4',
|
82
|
+
type: 'line',
|
83
|
+
dashStyle: 'LongDash',
|
84
|
+
yAxis: 1,
|
85
|
+
data: <%= other.to_json %>
|
86
|
+
},
|
87
|
+
]
|
88
|
+
});
|
89
|
+
//]]>
|
90
|
+
</script>
|
91
|
+
</body>
|
92
|
+
</html>
|
@@ -0,0 +1,90 @@
|
|
1
|
+
<%
|
2
|
+
title = "Impact by Week: #{username}"
|
3
|
+
%>
|
4
|
+
|
5
|
+
<html>
|
6
|
+
<head>
|
7
|
+
<title><%= title %></title>
|
8
|
+
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
9
|
+
<script src="http://code.highcharts.com/highcharts.js"></script>
|
10
|
+
<script src="http://code.highcharts.com/modules/exporting.js"></script>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>
|
14
|
+
|
15
|
+
<script type="text/javascript">
|
16
|
+
//<![CDATA[
|
17
|
+
var chart = new Highcharts.Chart({
|
18
|
+
chart: {
|
19
|
+
renderTo: 'container',
|
20
|
+
},
|
21
|
+
plotOptions: {
|
22
|
+
series: {
|
23
|
+
shadow: false,
|
24
|
+
lineWidth: 1,
|
25
|
+
marker: {
|
26
|
+
enabled: false
|
27
|
+
}
|
28
|
+
}
|
29
|
+
},
|
30
|
+
title: {
|
31
|
+
text: '<%= title %>'
|
32
|
+
},
|
33
|
+
xAxis: {
|
34
|
+
categories: <%= labels.to_json %>
|
35
|
+
},
|
36
|
+
yAxis: [
|
37
|
+
{ // additions and deletions
|
38
|
+
title: {
|
39
|
+
text: ''
|
40
|
+
}
|
41
|
+
},
|
42
|
+
{ // total impact
|
43
|
+
title: {
|
44
|
+
text: ''
|
45
|
+
},
|
46
|
+
labels: {
|
47
|
+
style: {
|
48
|
+
color: '#047DDA'
|
49
|
+
}
|
50
|
+
},
|
51
|
+
opposite: true,
|
52
|
+
min: 0
|
53
|
+
}
|
54
|
+
],
|
55
|
+
tooltip: {
|
56
|
+
formatter: function() {
|
57
|
+
if(this.series.name == 'Deletions')
|
58
|
+
return (-1 * this.y).toString();
|
59
|
+
else
|
60
|
+
return this.y.toString();
|
61
|
+
}
|
62
|
+
},
|
63
|
+
credits: {
|
64
|
+
enabled: false
|
65
|
+
},
|
66
|
+
series: [{
|
67
|
+
name: 'Additions',
|
68
|
+
color: '#1DB34F',
|
69
|
+
type: 'area',
|
70
|
+
data: <%= additions.to_json %>
|
71
|
+
}, {
|
72
|
+
name: 'Deletions',
|
73
|
+
color: '#AD1017',
|
74
|
+
type: 'area',
|
75
|
+
data: <%= deletions.collect{|d| -1*d }.to_json %>
|
76
|
+
},
|
77
|
+
{
|
78
|
+
name: 'Total Impact',
|
79
|
+
color: '#047DDA',
|
80
|
+
type: 'line',
|
81
|
+
//dashStyle: 'LongDash',
|
82
|
+
yAxis: 1,
|
83
|
+
data: <%= impacts.to_json %>
|
84
|
+
},
|
85
|
+
]
|
86
|
+
});
|
87
|
+
//]]>
|
88
|
+
</script>
|
89
|
+
</body>
|
90
|
+
</html>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
<%
|
2
|
+
data_type = "commits" if data_type == "count"
|
3
|
+
title = "#{data_type.titleize} by Repository: #{username}"
|
4
|
+
%>
|
5
|
+
|
6
|
+
<html>
|
7
|
+
<head>
|
8
|
+
<title><%= title %></title>
|
9
|
+
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
10
|
+
<script src="http://code.highcharts.com/highcharts.js"></script>
|
11
|
+
<script src="http://code.highcharts.com/modules/exporting.js"></script>
|
12
|
+
</head>
|
13
|
+
<body>
|
14
|
+
<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>
|
15
|
+
|
16
|
+
<script type="text/javascript">
|
17
|
+
//<![CDATA[
|
18
|
+
var chart = new Highcharts.Chart({
|
19
|
+
chart: {
|
20
|
+
renderTo: 'container',
|
21
|
+
plotBackgroundColor: null,
|
22
|
+
plotBorderWidth: null,
|
23
|
+
plotShadow: false
|
24
|
+
},
|
25
|
+
title: {
|
26
|
+
text: '<%= title %>'
|
27
|
+
},
|
28
|
+
tooltip: {
|
29
|
+
pointFormat: '{series.name}: <b>{point.percentage}%</b>',
|
30
|
+
percentageDecimals: 1
|
31
|
+
},
|
32
|
+
plotOptions: {
|
33
|
+
pie: {
|
34
|
+
allowPointSelect: true,
|
35
|
+
cursor: 'pointer',
|
36
|
+
dataLabels: {
|
37
|
+
enabled: false
|
38
|
+
},
|
39
|
+
showInLegend: true
|
40
|
+
}
|
41
|
+
},
|
42
|
+
series: [{
|
43
|
+
type: 'pie',
|
44
|
+
name: 'Repositories',
|
45
|
+
data: <%= data.to_json %>
|
46
|
+
}]
|
47
|
+
});
|
48
|
+
//]]>
|
49
|
+
</script>
|
50
|
+
</body>
|
51
|
+
</html>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module Hubtime
|
4
|
+
class Commit
|
5
|
+
attr_reader :repo_name, :sha, :additions, :deletions, :committer, :time
|
6
|
+
def initialize(hash, repo_name = nil, committer = nil)
|
7
|
+
@repo_name = repo_name # or figure it out
|
8
|
+
@sha = hash["sha"]
|
9
|
+
@additions = hash["stats"]["additions"].to_i
|
10
|
+
@deletions = hash["stats"]["deletions"].to_i
|
11
|
+
@committer = committer # or figure it out
|
12
|
+
@time = parse_time(hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{time.strftime('%F')} commit: #{repo_name} : #{sha} by #{committer} with #{additions} additions and #{deletions} deletions}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def count
|
20
|
+
return 0 if impact <= 0
|
21
|
+
1
|
22
|
+
end
|
23
|
+
|
24
|
+
def impact
|
25
|
+
additions + deletions
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def parse_time(hash)
|
32
|
+
return nil unless hash["commit"]
|
33
|
+
if hash["commit"]["author"]
|
34
|
+
return Time.parse(hash["commit"]["author"]["date"]) if hash["commit"]["author"]["date"]
|
35
|
+
end
|
36
|
+
if hash["commit"]["committer"]
|
37
|
+
return Time.parse(hash["commit"]["committer"]["date"]) if hash["commit"]["committer"]["date"]
|
38
|
+
end
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
Octokit.user_agent = "Hubtime : Octokit Ruby Gem #{Octokit::VERSION}"
|
3
|
+
|
4
|
+
module Hubtime
|
5
|
+
class GithubService
|
6
|
+
def self.owner
|
7
|
+
@owner ||= self.new
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :client
|
11
|
+
def initialize
|
12
|
+
@client = Octokit::Client.new(:login => HubConfig.user, :password => HubConfig.password, :auto_traversal => true)
|
13
|
+
# puts @client.ratelimit_remaining
|
14
|
+
end
|
15
|
+
|
16
|
+
def commits(username, start_time, end_time, &block)
|
17
|
+
self.repositories(username).each do |repo_name|
|
18
|
+
puts "#{repo_name}"
|
19
|
+
repo = Repo.new(repo_name, username, start_time, end_time)
|
20
|
+
repo.commits do |commit|
|
21
|
+
block.call commit
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def repositories(username)
|
27
|
+
repos = []
|
28
|
+
|
29
|
+
client.repositories.each do |hash|
|
30
|
+
repos << hash.full_name
|
31
|
+
end
|
32
|
+
|
33
|
+
unless username == client.login
|
34
|
+
client.repositories(username).each do |hash|
|
35
|
+
repos << hash.full_name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
self.organizations(username).each do |org_name|
|
40
|
+
client.organization_repositories(org_name).each do |hash|
|
41
|
+
repos << hash.full_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# return these, ignoring requested ones
|
46
|
+
repos.compact.uniq - HubConfig.ignore
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def organizations(username)
|
52
|
+
names = []
|
53
|
+
|
54
|
+
client.organizations.each do |hash|
|
55
|
+
names << hash.login
|
56
|
+
end
|
57
|
+
|
58
|
+
unless username == client.login
|
59
|
+
client.organizations(username).each do |hash|
|
60
|
+
names << hash.login
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
names.compact.uniq
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha2'
|
5
|
+
|
6
|
+
module Hubtime
|
7
|
+
class HubConfig
|
8
|
+
def self.instance
|
9
|
+
@config ||= HubConfig.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.auth(user, password)
|
13
|
+
store(user, password)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.store(user, password)
|
17
|
+
instance.store(user, password)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.user
|
21
|
+
instance.user
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.password
|
25
|
+
instance.password
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.display_password
|
29
|
+
'*' * password.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.ignore
|
33
|
+
instance.ignore
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.add_ignore(repo_name)
|
37
|
+
instance.add_ignore(repo_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.threads
|
41
|
+
8
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :user, :password, :ignore
|
45
|
+
def initialize
|
46
|
+
@file_name = "config"
|
47
|
+
hash = read_file
|
48
|
+
@user = hash["user"]
|
49
|
+
@password = Stuff.decrypt(hash["password"])
|
50
|
+
@ignore = hash["ignore"] || []
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_file
|
54
|
+
YAML.load_file(file)
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_file!
|
58
|
+
hash = {}
|
59
|
+
["user", "password", "ignore"].each do |key|
|
60
|
+
hash[key] = instance_variable_get("@#{key}")
|
61
|
+
end
|
62
|
+
|
63
|
+
hash["password"] = Stuff.encrypt(hash["password"])
|
64
|
+
|
65
|
+
File.open(file, 'w' ) do |out|
|
66
|
+
YAML.dump(hash, out)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def file
|
71
|
+
file = File.join(File.expand_path("."), "hubtime_config.yml")
|
72
|
+
|
73
|
+
unless File.exists?(file)
|
74
|
+
File.open(file, 'w' ) do |out|
|
75
|
+
YAML.dump({}, out )
|
76
|
+
end
|
77
|
+
end
|
78
|
+
file
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_ignore(repo_name)
|
82
|
+
@ignore << repo_name
|
83
|
+
@ignore.uniq!
|
84
|
+
write_file!
|
85
|
+
end
|
86
|
+
|
87
|
+
def store(user, password)
|
88
|
+
@user = user
|
89
|
+
@password = password
|
90
|
+
write_file!
|
91
|
+
end
|
92
|
+
|
93
|
+
class Stuff
|
94
|
+
def self.key
|
95
|
+
sha256 = Digest::SHA2.new(256)
|
96
|
+
sha256.digest("better than plain text")
|
97
|
+
end
|
98
|
+
def self.iv
|
99
|
+
"kjfdhkkkjkjhfdskljghfkdjhags"
|
100
|
+
end
|
101
|
+
def self.encrypt(payload)
|
102
|
+
return nil if payload == nil
|
103
|
+
aes = OpenSSL::Cipher.new("AES-256-CFB")
|
104
|
+
aes.encrypt
|
105
|
+
aes.key = key
|
106
|
+
aes.iv = iv
|
107
|
+
|
108
|
+
aes.update(payload) + aes.final
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.decrypt(encrypted)
|
112
|
+
return nil if encrypted == nil
|
113
|
+
|
114
|
+
aes = OpenSSL::Cipher.new("AES-256-CFB")
|
115
|
+
aes.decrypt
|
116
|
+
aes.key = key
|
117
|
+
aes.iv = iv
|
118
|
+
aes.update(encrypted) + aes.final
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|