hubtime 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|