git_pm 0.0.1

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