git_pm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/bin/git_pm +20 -0
- data/lib/SVG/Graph/Bar.rb +148 -0
- data/lib/SVG/Graph/BarBase.rb +139 -0
- data/lib/SVG/Graph/BarHorizontal.rb +149 -0
- data/lib/SVG/Graph/Graph.rb +978 -0
- data/lib/SVG/Graph/Line.rb +444 -0
- data/lib/SVG/Graph/Pie.rb +347 -0
- data/lib/SVG/Graph/Plot.rb +500 -0
- data/lib/SVG/Graph/Schedule.rb +373 -0
- data/lib/SVG/Graph/TimeSeries.rb +241 -0
- data/lib/git_pm.rb +30 -0
- data/lib/git_pm/commits_by_dev.rb +81 -0
- data/lib/git_pm/commits_per_month.rb +76 -0
- data/lib/git_pm/dir_lines_by_author.rb +99 -0
- data/lib/git_pm/git.rb +67 -0
- data/lib/git_pm/git_data.rb +86 -0
- data/lib/git_pm/lines_by_author.rb +47 -0
- data/spec/git_pm_spec.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- metadata +94 -0
data/lib/git_pm.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'open3'
|
3
|
+
require 'date'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'time'
|
6
|
+
require 'set'
|
7
|
+
require 'git_pm/git'
|
8
|
+
require 'git_pm/git_data'
|
9
|
+
require 'SVG/Graph/Pie'
|
10
|
+
require 'SVG/Graph/BarHorizontal'
|
11
|
+
require 'SVG/Graph/TimeSeries'
|
12
|
+
require 'SVG/Graph/Line'
|
13
|
+
|
14
|
+
require 'git_pm/lines_by_author'
|
15
|
+
require 'git_pm/commits_by_dev'
|
16
|
+
require 'git_pm/commits_per_month'
|
17
|
+
|
18
|
+
|
19
|
+
module GitPm
|
20
|
+
def self.script(id, opts)
|
21
|
+
case id
|
22
|
+
when "lines-by-author"
|
23
|
+
GitPm::LinesByAuthor.new(opts)
|
24
|
+
when "commits-per-month"
|
25
|
+
GitPm::CommitsPerMonth.new(opts)
|
26
|
+
when "commits-by-dev"
|
27
|
+
GitPm::CommitsByDev.new(opts)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module GitPm
|
2
|
+
class CommitsByDev
|
3
|
+
def initialize(args)
|
4
|
+
@branch = args.shift || "master"
|
5
|
+
end
|
6
|
+
|
7
|
+
def run!
|
8
|
+
data = `git shortlog --date=short --pretty=format:"%at %s" --no-merges --since="3 months ago" #{@branch}`
|
9
|
+
current = nil
|
10
|
+
stats = {}
|
11
|
+
fields = Set.new
|
12
|
+
data.each do |line|
|
13
|
+
if line =~ /^\s{6}(\d+)\s(.+)/
|
14
|
+
date = Time.at($1.to_i)
|
15
|
+
msg = $2
|
16
|
+
|
17
|
+
stats[current] ||= {}
|
18
|
+
(stats[current][get_day(date)] ||= []) << msg
|
19
|
+
fields << Time.parse(date.strftime("%F"))
|
20
|
+
elsif line =~ /^(\S.+)\s\(\d+\):/
|
21
|
+
current = $1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
fields = fields.to_a.sort
|
26
|
+
by_date = fields.map {|e| get_day(e) }
|
27
|
+
|
28
|
+
graph = SVG::Graph::Line.new({:height => 500, :width => 1800*3, :fields => by_date})
|
29
|
+
|
30
|
+
stats.each do |name, all_commits|
|
31
|
+
data = by_date.map {|f| (all_commits[f] || []).size }
|
32
|
+
|
33
|
+
graph.add_data({:data => data, :title => name})
|
34
|
+
end
|
35
|
+
|
36
|
+
graph.burn
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_day(date)
|
40
|
+
month = date.strftime("%B")
|
41
|
+
|
42
|
+
day = date.day
|
43
|
+
if date.hour < 6
|
44
|
+
day -= 1
|
45
|
+
end
|
46
|
+
|
47
|
+
if day == 0
|
48
|
+
nd = (Time.parse(date.strftime("%F"))-1)
|
49
|
+
day = nd.day
|
50
|
+
month = nd.strftime("%B")
|
51
|
+
end
|
52
|
+
|
53
|
+
"#{day} #{month}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_xls(by_date, stats)
|
57
|
+
report = Spreadsheet::Workbook.new
|
58
|
+
|
59
|
+
stats.each do |name, commits_by_date|
|
60
|
+
sheet = report.create_worksheet(:name => name)
|
61
|
+
sheet.column(0).width = 80
|
62
|
+
row = 0
|
63
|
+
|
64
|
+
bold = Spreadsheet::Format.new :weight => :bold
|
65
|
+
by_date.each do |date|
|
66
|
+
commits = commits_by_date[date] || []
|
67
|
+
|
68
|
+
sheet.row(row+=1).push date
|
69
|
+
commits.each do |commit|
|
70
|
+
sheet.row(row+=1).push commit
|
71
|
+
end
|
72
|
+
|
73
|
+
sheet.row(row+=1).push ""
|
74
|
+
sheet.row(row+=1).push ""
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
report.write("commits_by_day.xls")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module GitPm
|
4
|
+
class CommitsPerMonth
|
5
|
+
def initialize(options)
|
6
|
+
@branch = options.shift || "master"
|
7
|
+
end
|
8
|
+
|
9
|
+
def run!
|
10
|
+
h = Hash.new
|
11
|
+
linen = 0
|
12
|
+
dategroup = Date.new
|
13
|
+
`git log --pretty=format:%aD --shortstat #{@branch} | grep [a-zA-Z0-9]`.each do |line|
|
14
|
+
if line =~ /\d\d:\d\d:\d\d/ then
|
15
|
+
date = Date.parse(line)
|
16
|
+
dategroup = Date.new(date.year, date.month, 1)
|
17
|
+
if h[dategroup] then
|
18
|
+
h[dategroup][:commits] += 1
|
19
|
+
else
|
20
|
+
h[dategroup] = Hash.new
|
21
|
+
h[dategroup][:commits] = 1
|
22
|
+
end
|
23
|
+
else
|
24
|
+
line =~ /([\d]+) files changed, ([\d]+) insertions\(\+\), ([\d]+) deletions\(-\)/
|
25
|
+
h[dategroup][:insert] = (h[dategroup][:insert])? h[dategroup][:insert].to_i + $2.to_i : $2.to_i
|
26
|
+
h[dategroup][:delete] = (h[dategroup][:delete])? h[dategroup][:delete].to_i + $3.to_i : $3.to_i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
CSV.open("/tmp/count.csv", "w") do |csv|
|
31
|
+
h.sort.each do |date, value|
|
32
|
+
csv << [ "#{date.month}/1/#{date.year}", value[:commits], value[:insert] + value[:delete] ]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
commits = Array.new
|
37
|
+
h.each_pair do |date,value|
|
38
|
+
commits.push( date.to_s )
|
39
|
+
commits.push( value[:commits] )
|
40
|
+
end
|
41
|
+
|
42
|
+
graph = SVG::Graph::TimeSeries.new({
|
43
|
+
:width => 640,
|
44
|
+
:height => 480,
|
45
|
+
:graph_title => "Commits/Month",
|
46
|
+
:show_graph_title => true,
|
47
|
+
:no_css => false,
|
48
|
+
:key => false,
|
49
|
+
:scale_x_integers => true,
|
50
|
+
:scale_y_integers => true,
|
51
|
+
:show_data_labels => false,
|
52
|
+
:show_x_guidelines => true,
|
53
|
+
:show_x_title => true,
|
54
|
+
:x_title => "Months",
|
55
|
+
:show_y_title => true,
|
56
|
+
:y_title => "Commits",
|
57
|
+
:y_title_text_direction => :bt,
|
58
|
+
:stagger_x_labels => true,
|
59
|
+
:x_label_format => "%m/%d/%y",
|
60
|
+
})
|
61
|
+
|
62
|
+
if commits.size < 2
|
63
|
+
$stderr.puts "not enought data to generate the graph"
|
64
|
+
return ""
|
65
|
+
end
|
66
|
+
graph.add_data({
|
67
|
+
:data => commits,
|
68
|
+
:title => 'commits',
|
69
|
+
})
|
70
|
+
|
71
|
+
graph.burn
|
72
|
+
end
|
73
|
+
end #CommitsPerMonth
|
74
|
+
end
|
75
|
+
|
76
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
|
2
|
+
$:.unshift File.dirname(__FILE__) + "/../common/ruby"
|
3
|
+
|
4
|
+
require 'gitdata'
|
5
|
+
require 'SVG/Graph/Pie'
|
6
|
+
require 'SVG/Graph/BarHorizontal'
|
7
|
+
|
8
|
+
branch = "master"
|
9
|
+
dirs = ARGV.dup
|
10
|
+
|
11
|
+
lines_by_author = {}
|
12
|
+
|
13
|
+
wd = GitData.find_working_dir
|
14
|
+
exit -1 if not wd
|
15
|
+
|
16
|
+
rx = /^#{Regexp.escape(wd)}\//
|
17
|
+
dirs.map! do |w|
|
18
|
+
w = File.expand_path(w)
|
19
|
+
if w == wd
|
20
|
+
""
|
21
|
+
else
|
22
|
+
w.sub(rx, "")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Dir.chdir wd do
|
27
|
+
git = Git.new
|
28
|
+
git.ls_tree({:r => true, :name_only => true}, branch) do |stdout, stderr|
|
29
|
+
stdout.each { |filename|
|
30
|
+
parse = false
|
31
|
+
dirs.each { |expat|
|
32
|
+
len = expat.length
|
33
|
+
if filename[0, len] == expat
|
34
|
+
parse = true
|
35
|
+
break
|
36
|
+
end
|
37
|
+
}
|
38
|
+
next unless parse
|
39
|
+
|
40
|
+
lines = 0
|
41
|
+
committer = nil
|
42
|
+
|
43
|
+
filename = filename[0,filename.length-1]
|
44
|
+
add_to_current = false
|
45
|
+
modified_by = {}
|
46
|
+
git.blame({:incremental => true}, filename) { |stdout, stderr|
|
47
|
+
stdout.each { |line|
|
48
|
+
if line =~ /\w{40}\s(\d+)\s(\d+)\s(\d+)/
|
49
|
+
source_line = $1
|
50
|
+
result_line = $2
|
51
|
+
lines = $3.to_i
|
52
|
+
add_to_current = true
|
53
|
+
elsif line =~ /^committer\s(.+)/
|
54
|
+
committer = $1
|
55
|
+
modified_by[committer] = true
|
56
|
+
lines_by_author[committer] ||= {:lines => 0, :files => 0}
|
57
|
+
lines_by_author[committer][:lines] += lines
|
58
|
+
add_to_current = false
|
59
|
+
elsif line =~ /^committer-mail\s\<(.+)\>/
|
60
|
+
lines_by_author[committer][:email] = $1
|
61
|
+
elsif line =~ /^filename/
|
62
|
+
lines_by_author[committer][:lines] += lines if add_to_current
|
63
|
+
end
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
modified_by.each { |author, v|
|
68
|
+
lines_by_author[author][:files] += 1
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
fields = []
|
75
|
+
lba = []
|
76
|
+
|
77
|
+
authors = 0
|
78
|
+
lines_by_author.sort_by { |k, v| v[:lines] }.each { |value|
|
79
|
+
fields << value.first
|
80
|
+
lba << value.last[:lines]
|
81
|
+
|
82
|
+
authors += 1
|
83
|
+
}
|
84
|
+
|
85
|
+
graph = SVG::Graph::BarHorizontal.new({
|
86
|
+
:height => 40*authors,
|
87
|
+
:width => 640+20*authors,
|
88
|
+
:fields => fields,
|
89
|
+
:rotate_y_labels => false
|
90
|
+
})
|
91
|
+
|
92
|
+
graph.add_data({
|
93
|
+
:data => lba,
|
94
|
+
:title => 'Lines by author',
|
95
|
+
})
|
96
|
+
|
97
|
+
File.open("#{branch}_lines_by_author.svg", "w") do |file|
|
98
|
+
file << graph.burn()
|
99
|
+
end
|
data/lib/git_pm/git.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module GitPm
|
2
|
+
|
3
|
+
class Git
|
4
|
+
undef_method :clone
|
5
|
+
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@binary = "git"
|
9
|
+
@preopt_args = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# git-dir, --exec-path, --paginate, --bare
|
13
|
+
def set_options!(options)
|
14
|
+
@preopt_args = transform_options(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(cmd, opts = {}, *args)
|
18
|
+
opt_args = transform_options(opts)
|
19
|
+
ext_args = args.map { |a| if not a.to_s.empty? then (a == '--' or a =~ /^\s*\|/) ? a : "'#{shell_escape(a)}'" end }
|
20
|
+
|
21
|
+
call = "#{@binary} #{@preopt_args} #{cmd.to_s.gsub(/_/, '-')} #{(opt_args + ext_args).join(' ')}"
|
22
|
+
|
23
|
+
$stderr.puts call if $DEBUG
|
24
|
+
stdin, stdout, stderr = Open3.popen3(call)
|
25
|
+
|
26
|
+
if block_given?
|
27
|
+
yield stdout, stderr
|
28
|
+
end
|
29
|
+
|
30
|
+
lines = stdout.readlines
|
31
|
+
|
32
|
+
stdout.close
|
33
|
+
stderr.close
|
34
|
+
stdin.close
|
35
|
+
|
36
|
+
lines
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
protected
|
41
|
+
def shell_escape(str)
|
42
|
+
str.to_s.gsub("'", '"')
|
43
|
+
end
|
44
|
+
|
45
|
+
def transform_options(options, unif = '=')
|
46
|
+
args = []
|
47
|
+
options.keys.each do |opt|
|
48
|
+
if opt.to_s.size == 1
|
49
|
+
if options[opt] == true
|
50
|
+
args << "-#{opt}"
|
51
|
+
else
|
52
|
+
val = options.delete(opt)
|
53
|
+
args << "-#{opt.to_s} '#{shell_escape(val)}'"
|
54
|
+
end
|
55
|
+
else
|
56
|
+
if options[opt] == true
|
57
|
+
args << "--#{opt.to_s.gsub(/_/, '-')}"
|
58
|
+
else
|
59
|
+
val = options.delete(opt)
|
60
|
+
args << "--#{opt.to_s.gsub(/_/, '-')}#{unif}'#{shell_escape(val)}'"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
class GitData
|
2
|
+
def self.get_data(head = "master")
|
3
|
+
data = []
|
4
|
+
|
5
|
+
git = GitPm::Git.new
|
6
|
+
|
7
|
+
git.rev_list({:pretty => "format:name:%cn%nemail:%ce%ntimestamp:%ct", :no_merges => true}, head) do |stdout, stderr|
|
8
|
+
stdout.each { |line|
|
9
|
+
if line =~ /^commit/
|
10
|
+
data << {}
|
11
|
+
elsif line =~ /^(\w+):(.+)$/
|
12
|
+
data.last[$1] = $2
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
data
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.blame(head = "master", exclude = [])
|
21
|
+
lines_by_author = {}
|
22
|
+
|
23
|
+
wd = self.find_working_dir
|
24
|
+
return lines_by_author if not wd
|
25
|
+
|
26
|
+
Dir.chdir wd do
|
27
|
+
git = GitPm::Git.new
|
28
|
+
git.ls_tree({:r => true, :name_only => true}, head) do |stdout, stderr|
|
29
|
+
stdout.each { |filename|
|
30
|
+
parse = true
|
31
|
+
exclude.each { |expat|
|
32
|
+
len = expat.length
|
33
|
+
if filename[0, len] == expat
|
34
|
+
parse = false
|
35
|
+
break
|
36
|
+
end
|
37
|
+
}
|
38
|
+
next unless parse
|
39
|
+
|
40
|
+
lines = 0
|
41
|
+
committer = nil
|
42
|
+
|
43
|
+
filename = filename[0,filename.length-1]
|
44
|
+
add_to_current = false
|
45
|
+
modified_by = {}
|
46
|
+
git.blame({:incremental => true}, filename) { |stdout, stderr|
|
47
|
+
stdout.each { |line|
|
48
|
+
if line =~ /\w{40}\s(\d+)\s(\d+)\s(\d+)/
|
49
|
+
source_line = $1
|
50
|
+
result_line = $2
|
51
|
+
lines = $3.to_i
|
52
|
+
add_to_current = true
|
53
|
+
elsif line =~ /^committer\s(.+)/
|
54
|
+
committer = $1
|
55
|
+
modified_by[committer] = true
|
56
|
+
lines_by_author[committer] ||= {:lines => 0, :files => 0}
|
57
|
+
lines_by_author[committer][:lines] += lines
|
58
|
+
add_to_current = false
|
59
|
+
elsif line =~ /^committer-mail\s\<(.+)\>/
|
60
|
+
lines_by_author[committer][:email] = $1
|
61
|
+
elsif line =~ /^filename/
|
62
|
+
lines_by_author[committer][:lines] += lines if add_to_current
|
63
|
+
end
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
modified_by.each { |author, v|
|
68
|
+
lines_by_author[author][:files] += 1
|
69
|
+
}
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
lines_by_author
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.find_working_dir
|
78
|
+
wd = Dir.getwd
|
79
|
+
loop do
|
80
|
+
return File.expand_path(wd) if File.exist?(File.join(wd, ".git"))
|
81
|
+
wd = File.join(wd, "..")
|
82
|
+
end
|
83
|
+
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module GitPm
|
2
|
+
class LinesByAuthor
|
3
|
+
def initialize(options)
|
4
|
+
@branch = options.shift || "master"
|
5
|
+
end
|
6
|
+
|
7
|
+
def run!
|
8
|
+
exclude = []
|
9
|
+
|
10
|
+
if File.directory?("vendor")
|
11
|
+
exclude << "vendor"
|
12
|
+
end
|
13
|
+
|
14
|
+
if File.directory?("3rdparty")
|
15
|
+
exclude << "3rdparty"
|
16
|
+
end
|
17
|
+
|
18
|
+
data = GitData.blame(@branch, exclude)
|
19
|
+
|
20
|
+
fields = []
|
21
|
+
lines_by_author = []
|
22
|
+
authors = 0
|
23
|
+
data.sort_by { |k, v| v[:lines] }.each { |value|
|
24
|
+
author = value.first
|
25
|
+
d = value.last
|
26
|
+
fields << author
|
27
|
+
lines_by_author << d[:lines]
|
28
|
+
|
29
|
+
authors += 1
|
30
|
+
}
|
31
|
+
|
32
|
+
graph = SVG::Graph::BarHorizontal.new({
|
33
|
+
:height => 40*authors,
|
34
|
+
:width => 640+20*authors,
|
35
|
+
:fields => fields,
|
36
|
+
:rotate_y_labels => false
|
37
|
+
})
|
38
|
+
|
39
|
+
graph.add_data({
|
40
|
+
:data => lines_by_author,
|
41
|
+
:title => 'Lines by author',
|
42
|
+
})
|
43
|
+
|
44
|
+
graph.burn()
|
45
|
+
end
|
46
|
+
end # LinesByAuthor
|
47
|
+
end
|