git_statistics 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/git-statistics +2 -1
- data/bin/git_statistics +2 -1
- data/lib/git_statistics.rb +69 -21
- data/lib/git_statistics/branches.rb +35 -0
- data/lib/git_statistics/collector.rb +20 -45
- data/lib/git_statistics/commit_line_extractor.rb +8 -8
- data/lib/git_statistics/commits.rb +7 -4
- data/lib/git_statistics/formatters/console.rb +50 -47
- data/lib/git_statistics/initialize.rb +0 -3
- data/lib/git_statistics/log.rb +59 -0
- data/lib/git_statistics/pipe.rb +29 -0
- data/lib/git_statistics/utilities.rb +33 -23
- data/lib/git_statistics/version.rb +1 -1
- data/spec/branches_spec.rb +44 -0
- data/spec/collector_spec.rb +20 -41
- data/spec/commits_spec.rb +51 -47
- data/spec/formatters/console_spec.rb +25 -18
- data/spec/log_spec.rb +54 -0
- data/spec/pipe_spec.rb +59 -0
- data/spec/utilities_spec.rb +65 -63
- metadata +52 -9
- data/lib/git_statistics/core_ext/string.rb +0 -11
data/bin/git-statistics
CHANGED
data/bin/git_statistics
CHANGED
data/lib/git_statistics.rb
CHANGED
@@ -1,50 +1,98 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'optparse'
|
1
3
|
require 'git_statistics/initialize'
|
2
4
|
|
3
5
|
module GitStatistics
|
4
6
|
class GitStatistics
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
attr_reader :options
|
8
|
+
def initialize
|
9
|
+
@options = OpenStruct.new(
|
10
|
+
email: false,
|
11
|
+
merges: false,
|
12
|
+
pretty: false,
|
13
|
+
update: false,
|
14
|
+
sort: "commits",
|
15
|
+
top: 0,
|
16
|
+
branch: false,
|
17
|
+
verbose: false,
|
18
|
+
debug: false,
|
19
|
+
limit: 100
|
20
|
+
)
|
21
|
+
parse_options
|
17
22
|
end
|
18
23
|
|
19
24
|
def execute
|
25
|
+
if options.debug
|
26
|
+
Log.level = Logger::DEBUG
|
27
|
+
Log.use_debug
|
28
|
+
elsif options.verbose
|
29
|
+
Log.level = Logger::INFO
|
30
|
+
end
|
31
|
+
|
20
32
|
# Collect data (incremental or fresh) based on presence of old data
|
21
|
-
if
|
33
|
+
if options.update
|
22
34
|
# Ensure commit directory is present
|
23
|
-
collector = Collector.new(
|
24
|
-
commits_directory = collector.repo_path
|
35
|
+
collector = Collector.new(options.limit, false, options.pretty)
|
36
|
+
commits_directory = File.join(collector.repo_path, ".git_statistics")
|
25
37
|
FileUtils.mkdir_p(commits_directory)
|
26
38
|
file_count = Utilities.number_of_matching_files(commits_directory, /\d+\.json/) - 1
|
27
39
|
|
28
|
-
# Only use --since if there is data present
|
29
40
|
if file_count >= 0
|
30
41
|
time = Utilities.get_modified_time(commits_directory + "#{file_count}.json")
|
31
|
-
|
42
|
+
# Only use --since if there is data present
|
43
|
+
collector.collect(options.branch, "--since=\"#{time}\"")
|
32
44
|
collected = true
|
33
45
|
end
|
34
46
|
end
|
35
47
|
|
36
48
|
# If no data was collected as there was no present data then start fresh
|
37
49
|
unless collected
|
38
|
-
collector = Collector.new(
|
39
|
-
collector.collect(
|
50
|
+
collector = Collector.new(options.limit, true, options.pretty)
|
51
|
+
collector.collect(options.branch)
|
40
52
|
end
|
41
53
|
|
42
54
|
# Calculate statistics
|
43
|
-
collector.commits.calculate_statistics(
|
55
|
+
collector.commits.calculate_statistics(options.email, options.merges)
|
44
56
|
|
45
57
|
# Print results
|
46
58
|
results = Formatters::Console.new(collector.commits)
|
47
|
-
puts results.print_summary(
|
59
|
+
puts results.print_summary(options.sort, options.email, options.top)
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_options
|
63
|
+
OptionParser.new do |opt|
|
64
|
+
opt.version = VERSION
|
65
|
+
opt.on "-e", "--email", "Use author's email instead of name" do
|
66
|
+
options.email = true
|
67
|
+
end
|
68
|
+
opt.on "-m", "--merges", "Factor in merges when calculating statistics" do
|
69
|
+
options.merges = true
|
70
|
+
end
|
71
|
+
opt.on "-p", "--pretty", "Save the commits in git_repo/.git_statistics in pretty print (larger file size)" do
|
72
|
+
options.pretty = true
|
73
|
+
end
|
74
|
+
opt.on "-u", "--update", "Update saved commits with new data" do
|
75
|
+
options.update = true
|
76
|
+
end
|
77
|
+
opt.on "-s", "--sort TYPE", "Sort authors by {commits, additions, deletions, create, delete, rename, copy, merges}" do |type|
|
78
|
+
options.sort = type
|
79
|
+
end
|
80
|
+
opt.on "-t", "--top N", Float,"Show the top N authors in results" do |value|
|
81
|
+
options.top = value
|
82
|
+
end
|
83
|
+
opt.on "-b", "--branch", "Use current branch for statistics (otherwise all branches)" do
|
84
|
+
options.branch = true
|
85
|
+
end
|
86
|
+
opt.on "-v", "--verbose", "Verbose output (shows INFO level log statements)" do
|
87
|
+
options.verbose = true
|
88
|
+
end
|
89
|
+
opt.on "-d", "--debug", "Debug output (shows DEBUG level log statements)" do
|
90
|
+
options.debug = true
|
91
|
+
end
|
92
|
+
opt.on "-l", "--limit MAX_COMMITS", Float, "The maximum limit of commits to hold in memory at a time" do |number|
|
93
|
+
options.limit = number
|
94
|
+
end
|
95
|
+
end.parse!
|
48
96
|
end
|
49
97
|
end
|
50
98
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module GitStatistics
|
2
|
+
class Branches
|
3
|
+
|
4
|
+
CURRENT_BRANCH = /\A\*\s/
|
5
|
+
NO_BRANCH = /no branch/i
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
list.collect { |branch| branch.sub(CURRENT_BRANCH, "") }
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.current
|
12
|
+
return '(none)' if detached?
|
13
|
+
list.detect { |branch| branch =~ CURRENT_BRANCH }.sub(CURRENT_BRANCH, "")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.detached?
|
17
|
+
stripped.grep(NO_BRANCH).any?
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.list
|
23
|
+
stripped.reject { |b| b =~ NO_BRANCH }
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.stripped
|
27
|
+
pipe.map(&:strip)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.pipe
|
31
|
+
Pipe.new("git --no-pager branch --no-color")
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -1,38 +1,29 @@
|
|
1
1
|
module GitStatistics
|
2
2
|
class Collector
|
3
3
|
|
4
|
-
attr_accessor :repo, :repo_path, :commits_path, :commits
|
4
|
+
attr_accessor :repo, :repo_path, :commits_path, :commits
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@verbose = verbose
|
6
|
+
def initialize(limit, fresh, pretty)
|
8
7
|
@repo = Utilities.get_repository
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@repo_path = File.expand_path("..", @repo.path) + File::Separator
|
13
|
-
@commits_path = @repo_path + ".git_statistics" + File::Separator
|
8
|
+
@repo_path = File.expand_path("..", @repo.path)
|
9
|
+
@commits_path = File.join(@repo_path, ".git_statistics")
|
14
10
|
@commits = Commits.new(@commits_path, fresh, limit, pretty)
|
15
11
|
end
|
16
12
|
|
17
13
|
def collect(branch, time_since = "", time_until = "")
|
18
|
-
# Create pipe for git log to acquire branches
|
19
|
-
pipe = open("|git --no-pager branch --no-color")
|
20
|
-
|
21
14
|
# Collect branches to use for git log
|
22
|
-
branches = branch ? [
|
15
|
+
branches = branch ? [] : Branches.all
|
23
16
|
|
24
17
|
# Create pipe for the git log to acquire commits
|
25
|
-
pipe =
|
26
|
-
|
27
|
-
|
28
|
-
|
18
|
+
pipe = Pipe.new("git --no-pager log #{branches.join(' ')} --date=iso --reverse"\
|
19
|
+
" --no-color --find-copies-harder --numstat --encoding=utf-8"\
|
20
|
+
" --summary #{time_since} #{time_until}"\
|
21
|
+
" --format=\"%H,%an,%ae,%ad,%p\"")
|
29
22
|
|
30
23
|
# Use a buffer approach to queue up lines from the log for each commit
|
31
24
|
buffer = []
|
32
25
|
pipe.each do |line|
|
33
26
|
|
34
|
-
line = line.clean_for_authors
|
35
|
-
|
36
27
|
# Extract the buffer (commit) when we match ','x5 in the log format (delimeter)
|
37
28
|
if line.split(',').size == 5
|
38
29
|
|
@@ -44,7 +35,6 @@ module GitStatistics
|
|
44
35
|
|
45
36
|
# Save commits to file if size exceeds limit or forced
|
46
37
|
@commits.flush_commits
|
47
|
-
@repo = Utilities.get_repository
|
48
38
|
end
|
49
39
|
|
50
40
|
buffer << line
|
@@ -57,33 +47,18 @@ module GitStatistics
|
|
57
47
|
|
58
48
|
def fall_back_collect_commit(sha)
|
59
49
|
# Create pipe for the git log to acquire commits
|
60
|
-
pipe =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
buffer = pipe.map { |line| line.clean_for_authors }
|
50
|
+
pipe = Pipe.new("git --no-pager show #{sha} --date=iso --reverse"\
|
51
|
+
" --no-color --find-copies-harder --numstat --encoding=utf-8 "\
|
52
|
+
"--summary --format=\"%H,%an,%ae,%ad,%p\"")
|
65
53
|
|
66
54
|
# Check that the buffer has valid information (i.e., sha was valid)
|
67
|
-
if !
|
68
|
-
|
55
|
+
if !pipe.empty? && pipe.first.split(',').first == sha
|
56
|
+
pipe.to_a
|
69
57
|
else
|
70
|
-
|
58
|
+
[]
|
71
59
|
end
|
72
60
|
end
|
73
61
|
|
74
|
-
def collect_branches(pipe)
|
75
|
-
# Acquire all available branches from repository
|
76
|
-
branches = []
|
77
|
-
pipe.each do |line|
|
78
|
-
|
79
|
-
# Remove the '*' leading the current branch
|
80
|
-
line = line[1..-1] if line[0] == '*'
|
81
|
-
branches << line.clean_for_authors
|
82
|
-
end
|
83
|
-
|
84
|
-
return branches
|
85
|
-
end
|
86
|
-
|
87
62
|
def acquire_commit_data(line)
|
88
63
|
# Split up formated line
|
89
64
|
commit_info = line.split(',')
|
@@ -109,11 +84,11 @@ module GitStatistics
|
|
109
84
|
# Acquire general commit information
|
110
85
|
commit_data = acquire_commit_data(buffer[0])
|
111
86
|
|
112
|
-
|
87
|
+
Log.info "Extracting #{commit_data[:sha]}"
|
113
88
|
|
114
89
|
# Abort if the commit sha extracted form the buffer is invalid
|
115
90
|
if commit_data[:sha].scan(/[\d|a-f]{40}/)[0].nil?
|
116
|
-
|
91
|
+
Log.warn "Invalid buffer containing commit information"
|
117
92
|
return
|
118
93
|
end
|
119
94
|
|
@@ -122,7 +97,7 @@ module GitStatistics
|
|
122
97
|
|
123
98
|
# No files were changed in this commit, abort commit
|
124
99
|
if files.nil?
|
125
|
-
|
100
|
+
Log.debug "No files were changed"
|
126
101
|
return
|
127
102
|
end
|
128
103
|
|
@@ -134,9 +109,9 @@ module GitStatistics
|
|
134
109
|
if blob.instance_of?(Grit::Blob)
|
135
110
|
process_blob(commit_data[:data], blob, file)
|
136
111
|
elsif blob.instance_of?(Grit::Submodule)
|
137
|
-
|
112
|
+
Log.debug "Ignoring submodule #{blob.name}"
|
138
113
|
else
|
139
|
-
|
114
|
+
Log.warn "Problem processing file #{file[:file]}"
|
140
115
|
end
|
141
116
|
end
|
142
117
|
return commit_data[:data]
|
@@ -17,31 +17,31 @@ module GitStatistics
|
|
17
17
|
split_file = Utilities.split_old_new_file(changes[2], changes[3])
|
18
18
|
{:additions => changes[0].to_i,
|
19
19
|
:deletions => changes[1].to_i,
|
20
|
-
:file => split_file[:new_file]
|
21
|
-
:old_file => split_file[:old_file]
|
20
|
+
:file => split_file[:new_file],
|
21
|
+
:old_file => split_file[:old_file]}
|
22
22
|
end
|
23
23
|
return modified_or_renamed unless modified_or_renamed.empty?
|
24
24
|
|
25
25
|
AdditionsOrDeletions.if_matches(line) do |changes|
|
26
26
|
{:additions => changes[0].to_i,
|
27
27
|
:deletions => changes[1].to_i,
|
28
|
-
:file => changes[2]
|
28
|
+
:file => changes[2]}
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
def created_or_deleted
|
33
33
|
CreatedOrDeleted.if_matches(line) do |changes|
|
34
|
-
{:status => changes[0]
|
35
|
-
:file => changes[1]
|
34
|
+
{:status => changes[0],
|
35
|
+
:file => changes[1]}
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
def renamed_or_copied
|
40
40
|
RenamedOrCopied.if_matches(line) do |changes|
|
41
41
|
split_file = Utilities.split_old_new_file(changes[1], changes[2])
|
42
|
-
{:status => changes[0]
|
43
|
-
:old_file => split_file[:old_file]
|
44
|
-
:new_file => split_file[:new_file]
|
42
|
+
{:status => changes[0],
|
43
|
+
:old_file => split_file[:old_file],
|
44
|
+
:new_file => split_file[:new_file],
|
45
45
|
:similar => changes[3].to_i}
|
46
46
|
end
|
47
47
|
end
|
@@ -14,12 +14,11 @@ module GitStatistics
|
|
14
14
|
|
15
15
|
def clean
|
16
16
|
# Ensure the path exists
|
17
|
-
FileUtils.mkdir_p(
|
17
|
+
FileUtils.mkdir_p(path)
|
18
18
|
|
19
19
|
# Remove all files within path if saving
|
20
|
-
if
|
21
|
-
|
22
|
-
next if %w[. ..].include? file
|
20
|
+
if fresh
|
21
|
+
files_in_path.each do |file|
|
23
22
|
File.delete(File.join(path, file))
|
24
23
|
end
|
25
24
|
end
|
@@ -30,6 +29,10 @@ module GitStatistics
|
|
30
29
|
@totals[:languages] = {}
|
31
30
|
end
|
32
31
|
|
32
|
+
def files_in_path
|
33
|
+
Dir.entries(path).reject { |file| %w[. ..].include?(file) }
|
34
|
+
end
|
35
|
+
|
33
36
|
def flush_commits(force = false)
|
34
37
|
if size >= limit || force
|
35
38
|
file_count = Utilities.number_of_matching_files(path, /\d+\.json/)
|
@@ -2,7 +2,7 @@ module GitStatistics
|
|
2
2
|
module Formatters
|
3
3
|
class Console
|
4
4
|
|
5
|
-
attr_accessor :commits
|
5
|
+
attr_accessor :commits, :config
|
6
6
|
|
7
7
|
def initialize(commits)
|
8
8
|
@commits = commits
|
@@ -17,7 +17,7 @@ module GitStatistics
|
|
17
17
|
raise "Parameter for --sort is not valid" if data.nil?
|
18
18
|
|
19
19
|
# Create config
|
20
|
-
config = {:data => data,
|
20
|
+
@config = {:data => data,
|
21
21
|
:author_length => Utilities.max_length_in_list(data.keys, 17),
|
22
22
|
:language_length => Utilities.max_length_in_list(@commits.totals[:languages].keys, 8),
|
23
23
|
:sort => sort,
|
@@ -25,70 +25,73 @@ module GitStatistics
|
|
25
25
|
:top_n => top_n}
|
26
26
|
|
27
27
|
# Acquire formatting pattern for output
|
28
|
-
pattern = "%-#{config[:author_length]}s | %-#{config[:language_length]}s | %7s | %9s | %9s | %7s | %7s | %7s | %6s | %6s |"
|
29
|
-
config
|
30
|
-
return config
|
28
|
+
@pattern = "| %-#{config[:author_length]}s | %-#{config[:language_length]}s | %7s | %9s | %9s | %7s | %7s | %7s | %6s | %6s |"
|
29
|
+
config
|
31
30
|
end
|
32
31
|
|
33
32
|
def print_summary(sort, email, top_n = 0)
|
34
33
|
# Prepare and determine the config for the result summary based on parameters
|
34
|
+
commit_totals = @commits.totals
|
35
35
|
config = prepare_result_summary(sort, email, top_n)
|
36
36
|
|
37
37
|
# Print query/header information
|
38
|
-
output = print_header
|
38
|
+
output = print_header
|
39
39
|
|
40
40
|
# Print per author information
|
41
|
-
config[:data].each do |
|
42
|
-
output
|
43
|
-
|
44
|
-
value[:rename], value[:copy], value[:merges]]
|
45
|
-
output += "\n"
|
46
|
-
output += print_language_data(config[:pattern], value)
|
41
|
+
config[:data].each do |name, commit_data|
|
42
|
+
output << print_row(name, commit_data)
|
43
|
+
output << print_language_data(commit_data)
|
47
44
|
end
|
48
|
-
|
49
|
-
|
50
|
-
output
|
51
|
-
output
|
52
|
-
data = @commits.totals
|
53
|
-
output += config[:pattern] % ["Repository Totals", "", data[:commits],
|
54
|
-
data[:additions], data[:deletions], data[:create],
|
55
|
-
data[:delete], data[:rename], data[:copy], data[:merges]]
|
56
|
-
output += "\n"
|
57
|
-
output += print_language_data(config[:pattern], data)
|
58
|
-
return output
|
45
|
+
output << separator
|
46
|
+
output << print_row("Repository Totals", commit_totals)
|
47
|
+
output << print_language_data(commit_totals)
|
48
|
+
output.flatten.join("\n")
|
59
49
|
end
|
60
50
|
|
61
|
-
def print_language_data(
|
62
|
-
output =
|
51
|
+
def print_language_data(data)
|
52
|
+
output = []
|
63
53
|
# Print information of each language for the data
|
64
|
-
data[:languages].each do |
|
65
|
-
output
|
66
|
-
value[:create], value[:delete], value[:rename],
|
67
|
-
value[:copy], value[:merges]]
|
68
|
-
output += "\n"
|
54
|
+
data[:languages].each do |language, commit_data|
|
55
|
+
output << print_row("", commit_data, language)
|
69
56
|
end
|
70
|
-
|
57
|
+
output
|
58
|
+
end
|
59
|
+
|
60
|
+
def print_row(name, commit_info, language = '')
|
61
|
+
format_for_row(name, language, commit_info[:commits],
|
62
|
+
commit_info[:additions], commit_info[:deletions], commit_info[:create],
|
63
|
+
commit_info[:delete], commit_info[:rename], commit_info[:copy], commit_info[:merges])
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_header
|
67
|
+
output = []
|
68
|
+
output << get_author_info(@commits.stats.size)
|
69
|
+
output << get_header_info
|
70
|
+
output
|
71
71
|
end
|
72
72
|
|
73
|
-
def
|
74
|
-
|
73
|
+
def get_header_info
|
74
|
+
headers = []
|
75
|
+
headers << separator
|
76
|
+
headers << format_for_row('Name/Email', 'Language', 'Commits', 'Additions', 'Deletions', 'Creates', 'Deletes', 'Renames', 'Copies', 'Merges')
|
77
|
+
headers << separator
|
78
|
+
headers
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_for_row(*columns)
|
82
|
+
@pattern % columns
|
83
|
+
end
|
84
|
+
|
85
|
+
def separator
|
86
|
+
"-" * 89 + "-"*config[:author_length] + "-"*config[:language_length]
|
87
|
+
end
|
75
88
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
output += "Top #{config[:top_n]} authors(#{total_authors}) sorted by #{config[:sort]}\n"
|
80
|
-
else
|
81
|
-
output += "All authors(#{total_authors}) sorted by #{config[:sort]}\n"
|
89
|
+
def get_author_info(total_authors)
|
90
|
+
if config[:top_n] > 0 && config[:top_n] < total_authors
|
91
|
+
return "Top #{config[:top_n]} authors(#{total_authors}) sorted by #{config[:sort]}"
|
82
92
|
end
|
83
93
|
|
84
|
-
#
|
85
|
-
output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
|
86
|
-
output += "\n"
|
87
|
-
output += config[:pattern] % ['Name/Email', 'Language', 'Commits', 'Additions', 'Deletions', 'Creates', 'Deletes', 'Renames', 'Copies', 'Merges']
|
88
|
-
output += "\n"
|
89
|
-
output += "-"*87 + "-"*config[:author_length] + "-"*config[:language_length]
|
90
|
-
output += "\n"
|
91
|
-
return output
|
94
|
+
"All authors(#{total_authors}) sorted by #{config[:sort]}"
|
92
95
|
end
|
93
96
|
end
|
94
97
|
end
|