git_pm 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.
@@ -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