git_statistics 0.2.0 → 0.3.0
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/.travis.yml +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +15 -0
- data/Guardfile +7 -0
- data/README.md +7 -2
- data/git_statistics.gemspec +1 -0
- data/lib/git_statistics/collector.rb +130 -245
- data/lib/git_statistics/commits.rb +19 -24
- data/lib/git_statistics/initialize.rb +1 -0
- data/lib/git_statistics/results.rb +96 -0
- data/lib/git_statistics/utilities.rb +108 -0
- data/lib/git_statistics/version.rb +1 -1
- data/lib/git_statistics.rb +7 -4
- data/spec/collector_spec.rb +365 -0
- data/spec/commits_spec.rb +238 -0
- data/spec/fixtures/commit_buffer_changes.txt +9 -0
- data/spec/fixtures/commit_buffer_information.txt +1 -0
- data/spec/fixtures/commit_buffer_information_first.txt +1 -0
- data/spec/fixtures/commit_buffer_information_with_merge.txt +1 -0
- data/spec/fixtures/commit_buffer_whole.txt +6 -0
- data/spec/fixtures/git_many_branches.txt +2 -0
- data/spec/fixtures/git_zero_branches.txt +1 -0
- data/spec/fixtures/header_output.txt +4 -0
- data/spec/fixtures/language_data_output.txt +2 -0
- data/spec/fixtures/multiple_authors.json +1 -0
- data/spec/fixtures/single_author_pretty.json +79 -0
- data/spec/fixtures/summary_output.txt +17 -0
- data/spec/results_spec.rb +143 -0
- data/spec/utilities_spec.rb +169 -0
- metadata +54 -2
@@ -12,38 +12,28 @@ module GitStatistics
|
|
12
12
|
@totals[:languages] = {}
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def identify_authors_email
|
24
|
-
self.each do |key,value|
|
25
|
-
if not @author_list.include?(value[:author_email])
|
26
|
-
@author_list << value[:author_email]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def author_top_n_type(type, n=0)
|
32
|
-
n = 0 if n < 0
|
33
|
-
return nil if @stats == nil || !@stats.first[1].has_key?(type)
|
34
|
-
return @stats.sorted_hash {|a,b| b[1][type.to_sym] <=> a[1][type]}.to_a[0..n-1]
|
15
|
+
def author_top_n_type(type, top_n=0)
|
16
|
+
top_n = 0 if top_n < 0
|
17
|
+
return nil if @stats.size == 0
|
18
|
+
return nil if !@stats.first[1].has_key?(type)
|
19
|
+
return Hash[*@stats.sorted_hash {|a,b| b[1][type.to_sym] <=> a[1][type]}.to_a[0..top_n-1].flatten]
|
35
20
|
end
|
36
21
|
|
37
22
|
def calculate_statistics(email, merge)
|
38
23
|
|
24
|
+
# Check that there are statistics to calculate
|
25
|
+
if self.size == 0
|
26
|
+
puts "Cannot calculate statistics as there is no data"
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
39
30
|
# Identify authors and author type
|
40
31
|
if email
|
41
|
-
identify_authors_email
|
42
32
|
type = :author_email
|
43
33
|
else
|
44
|
-
identify_authors
|
45
34
|
type = :author
|
46
35
|
end
|
36
|
+
@author_list = Utilities.unique_data_in_hash(self, type)
|
47
37
|
|
48
38
|
# Initialize the stats hash
|
49
39
|
@author_list.each do |author|
|
@@ -53,7 +43,7 @@ module GitStatistics
|
|
53
43
|
|
54
44
|
# Collect the stats from each commit
|
55
45
|
self.each do |key,value|
|
56
|
-
if
|
46
|
+
if !merge && value[:merge]
|
57
47
|
next
|
58
48
|
else
|
59
49
|
|
@@ -88,11 +78,15 @@ module GitStatistics
|
|
88
78
|
if data[:languages][file[:language].to_sym] == nil
|
89
79
|
data[:languages][file[:language].to_sym] = Hash.new(0)
|
90
80
|
end
|
81
|
+
|
91
82
|
data[:languages][file[:language].to_sym][:additions] += file[:additions]
|
92
83
|
data[:languages][file[:language].to_sym][:deletions] += file[:deletions]
|
93
|
-
|
84
|
+
|
85
|
+
if file[:status] != nil
|
94
86
|
data[:languages][file[:language].to_sym][file[:status].to_sym] += 1
|
95
87
|
end
|
88
|
+
|
89
|
+
return data
|
96
90
|
end
|
97
91
|
|
98
92
|
def add_commit_stats(data, commit)
|
@@ -105,6 +99,7 @@ module GitStatistics
|
|
105
99
|
data[:delete] += commit[:delete] if commit[:delete] != nil
|
106
100
|
data[:rename] += commit[:rename] if commit[:rename] != nil
|
107
101
|
data[:copy] += commit[:copy] if commit[:copy] != nil
|
102
|
+
return data
|
108
103
|
end
|
109
104
|
|
110
105
|
def load(file)
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module GitStatistics
|
2
|
+
class Results
|
3
|
+
|
4
|
+
attr_accessor :commits
|
5
|
+
|
6
|
+
def initialize(commits)
|
7
|
+
@commits = commits
|
8
|
+
end
|
9
|
+
|
10
|
+
def prepare_result_summary(sort, email, top_n=0)
|
11
|
+
# Default to a 0 if given a negative number to display
|
12
|
+
top_n = 0 if top_n < 0
|
13
|
+
|
14
|
+
# Acquire data based on sorty type and top # to show
|
15
|
+
data = @commits.author_top_n_type(sort.to_sym, top_n)
|
16
|
+
if data == nil
|
17
|
+
raise "Parameter for --sort is not valid"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create config
|
21
|
+
config = {:data => data,
|
22
|
+
:author_length => Utilities.find_longest_length(data, 17),
|
23
|
+
:language_length => Utilities.find_longest_length(@commits.language_list, 8),
|
24
|
+
:sort => sort,
|
25
|
+
:email => email,
|
26
|
+
:top_n => top_n}
|
27
|
+
|
28
|
+
# Acquire formatting pattern for output
|
29
|
+
pattern = "%-#{config[:author_length]}s | %-#{config[:language_length]}s | %7s | %9s | %9s | %7s | %7s | %7s | %6s | %6s |"
|
30
|
+
config[:pattern] = pattern
|
31
|
+
return config
|
32
|
+
end
|
33
|
+
|
34
|
+
def print_summary(sort, email, top_n=0)
|
35
|
+
# Prepare and determine the config for the result summary based on parameters
|
36
|
+
config = prepare_result_summary(sort, email, top_n)
|
37
|
+
|
38
|
+
# Print query/header information
|
39
|
+
output = print_header(config)
|
40
|
+
|
41
|
+
# Print per author information
|
42
|
+
config[:data].each do |key,value|
|
43
|
+
output += config[:pattern] % [key, "", value[:commits], value[:additions],
|
44
|
+
value[:deletions], value[:create], value[:delete],
|
45
|
+
value[:rename], value[:copy], value[:merges]]
|
46
|
+
output += "\n"
|
47
|
+
output += print_language_data(config[:pattern], value)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Reprint query/header for repository information
|
51
|
+
output += "\n"
|
52
|
+
output += print_header(config)
|
53
|
+
data = @commits.totals
|
54
|
+
output += config[:pattern] % ["Repository Totals", "", data[:commits],
|
55
|
+
data[:additions], data[:deletions], data[:create],
|
56
|
+
data[:delete], data[:rename], data[:copy], data[:merges]]
|
57
|
+
output += "\n"
|
58
|
+
output += print_language_data(config[:pattern], data)
|
59
|
+
return output
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_language_data(pattern, data)
|
63
|
+
output = ""
|
64
|
+
# Print information of each language for the data
|
65
|
+
data[:languages].each do |key,value|
|
66
|
+
output += pattern % ["", key, "", value[:additions], value[:deletions],
|
67
|
+
value[:create], value[:delete], value[:rename],
|
68
|
+
value[:copy], value[:merges]]
|
69
|
+
output += "\n"
|
70
|
+
end
|
71
|
+
return output
|
72
|
+
end
|
73
|
+
|
74
|
+
def print_header(config)
|
75
|
+
total_authors = @commits.author_list.length
|
76
|
+
|
77
|
+
output = ""
|
78
|
+
# Print summary information of displayed results
|
79
|
+
if config[:top_n] > 0 and config[:top_n] < total_authors
|
80
|
+
output += "Top #{config[:top_n]} authors(#{total_authors}) sorted by #{config[:sort]}\n"
|
81
|
+
else
|
82
|
+
output += "All authors(#{total_authors}) sorted by #{config[:sort]}\n"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Print column headers
|
86
|
+
output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
|
87
|
+
output += "\n"
|
88
|
+
output += config[:pattern] % ['Name/Email', 'Language', 'Commits', 'Additions', 'Deletions', 'Creates', 'Deletes', 'Renames', 'Copies', 'Merges']
|
89
|
+
output += "\n"
|
90
|
+
output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
|
91
|
+
output += "\n"
|
92
|
+
return output
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module GitStatistics
|
2
|
+
module Utilities
|
3
|
+
def self.get_repository(path=Dir.pwd)
|
4
|
+
# Connect to git repository if it exists
|
5
|
+
directory = Pathname.new(path)
|
6
|
+
repo = nil
|
7
|
+
while !directory.root? do
|
8
|
+
begin
|
9
|
+
repo = Grit::Repo.new(directory)
|
10
|
+
return repo
|
11
|
+
rescue
|
12
|
+
directory = directory.parent
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_longest_length(list, max=nil)
|
18
|
+
return list if list == nil
|
19
|
+
list.each do |key,value|
|
20
|
+
max = key.length if max == nil || key.length > max
|
21
|
+
end
|
22
|
+
return max
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.unique_data_in_hash(data, type)
|
26
|
+
list = []
|
27
|
+
data.each do |key,value|
|
28
|
+
if not list.include?(value[type])
|
29
|
+
list << value[type]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
return list
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.clean_string(string)
|
36
|
+
return string.strip.force_encoding("iso-8859-1").encode("utf-8")
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.split_old_new_file(old, new)
|
40
|
+
# Split the old and new chunks up (separted by the =>)
|
41
|
+
split_old = old.split('{')
|
42
|
+
split_new = new.split('}')
|
43
|
+
|
44
|
+
# Handle recombine the file splits into their whole paths)
|
45
|
+
if split_old.size == 1 && split_new.size == 1
|
46
|
+
old_file = split_old[0]
|
47
|
+
new_file = split_new[0]
|
48
|
+
elsif split_new.size == 1
|
49
|
+
old_file = split_old[0] + split_old[1]
|
50
|
+
new_file = split_old[0] + split_new[0]
|
51
|
+
elsif split_old.size == 1
|
52
|
+
old_file = split_old[0] + split_new[1]
|
53
|
+
new_file = split_old[0] + split_new[0] + split_new[1]
|
54
|
+
else
|
55
|
+
old_file = split_old[0] + split_old[1] + split_new[1]
|
56
|
+
new_file = split_old[0] + split_new[0] + split_new[1]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return files, yet remove the '//' if present from combining splits
|
60
|
+
return {:old_file => old_file.gsub('//', '/'),
|
61
|
+
:new_file => new_file.gsub('//', '/')}
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.find_blob_in_tree(tree, file)
|
65
|
+
# Check If cannot find tree in commit or if we found a submodule as the changed file
|
66
|
+
if tree == nil
|
67
|
+
return nil
|
68
|
+
elsif file == nil
|
69
|
+
return nil
|
70
|
+
elsif tree.instance_of?(Grit::Submodule)
|
71
|
+
return tree
|
72
|
+
end
|
73
|
+
|
74
|
+
# If the blob is within the current directory (tree)
|
75
|
+
if file.size == 1
|
76
|
+
blob = tree / file.first
|
77
|
+
|
78
|
+
# Check if blob is nil (could not find changed file in tree)
|
79
|
+
if blob == nil
|
80
|
+
|
81
|
+
# Try looking for submodules as they cannot be found using tree / file notation
|
82
|
+
tree.contents.each do |content|
|
83
|
+
if file.first == content.name
|
84
|
+
return tree
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Exit through recusion with the base case of a nil tree/blob
|
89
|
+
return find_blob_in_tree(blob, file)
|
90
|
+
end
|
91
|
+
return blob
|
92
|
+
else
|
93
|
+
# Explore deeper in the tree to find the blob of the changed file
|
94
|
+
return find_blob_in_tree(tree / file.first, file[1..-1])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.get_modified_time(file)
|
99
|
+
if OS.mac?
|
100
|
+
Time.at(`stat -f %m commits.json`.to_i)
|
101
|
+
elsif OS.linux?
|
102
|
+
Time.at(`stat -c %Y commits.json`.to_i)
|
103
|
+
else
|
104
|
+
raise "Update on the Windows operating system is not supported"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/git_statistics.rb
CHANGED
@@ -7,7 +7,7 @@ module GitStatistics
|
|
7
7
|
opt :email, "Use author's email instead of name", :default => false
|
8
8
|
opt :merges, "Factor in merges when calculating statistics", :default => false
|
9
9
|
opt :save, "Save the commits as commits.json", :default => false
|
10
|
-
opt :pretty, "
|
10
|
+
opt :pretty, "Save the commits as commits.json in pretty print (larger file size)", :default => false
|
11
11
|
opt :load, "Load commits.json instead of re-collecting data", :default => false
|
12
12
|
opt :update, "Update commits.json with new data (same as save and load together)", :default => false
|
13
13
|
opt :sort, "Sort authors by {commits, additions, deletions, create, delete, rename, copy, merges}", :default => "commits"
|
@@ -29,17 +29,20 @@ module GitStatistics
|
|
29
29
|
|
30
30
|
# Collect incremental recent data
|
31
31
|
if @opts[:update]
|
32
|
-
|
32
|
+
time = Utilities.get_modified_time("commit.json")
|
33
|
+
collector.collect(@opts[:branch], "--since=\"#{time}\"")
|
33
34
|
end
|
34
35
|
|
35
36
|
# Save data
|
36
|
-
if @opts[:save] || @opts[:update]
|
37
|
+
if @opts[:save] || @opts[:update] || @opts[:pretty]
|
37
38
|
collector.commits.save("commits.json", @opts[:pretty])
|
38
39
|
end
|
39
40
|
|
40
41
|
collector.commits.calculate_statistics(@opts[:email], @opts[:merges])
|
41
42
|
|
42
|
-
|
43
|
+
# Print results
|
44
|
+
results = Results.new(collector.commits)
|
45
|
+
puts results.print_summary(@opts[:sort], @opts[:email], @opts[:top])
|
43
46
|
end
|
44
47
|
end
|
45
48
|
end
|