git_statistics 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/git-statistics +1 -1
- data/bin/git_statistics +1 -1
- data/lib/git_statistics.rb +33 -35
- data/lib/git_statistics/collector.rb +13 -16
- data/lib/git_statistics/commit_summary.rb +13 -16
- data/lib/git_statistics/commits.rb +34 -36
- data/lib/git_statistics/diff_summary.rb +13 -17
- data/lib/git_statistics/formatters/console.rb +22 -18
- data/lib/git_statistics/log.rb +3 -5
- data/lib/git_statistics/pipe.rb +1 -1
- data/lib/git_statistics/utilities.rb +7 -9
- data/lib/git_statistics/version.rb +1 -1
- data/spec/collector_spec.rb +107 -104
- data/spec/commit_summary_spec.rb +110 -84
- data/spec/commits_spec.rb +251 -218
- data/spec/formatters/console_spec.rb +141 -127
- data/spec/log_spec.rb +19 -21
- data/spec/pipe_spec.rb +30 -25
- data/spec/utilities_spec.rb +42 -40
- metadata +37 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73f7fc388f1d121e11c2699b107138fee200da9b
|
4
|
+
data.tar.gz: b87b3b4c7cfbc545df2e7d933142289d0665c1e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c531508caedd57a36a3c0ef0ecb40db56483b17e28d765967496e3220b1752ff2091f349da6058f88ecdf41d6cb98cb97b1b7585c25c2db6da5520dbb2ad27e
|
7
|
+
data.tar.gz: 80370289c3c487d77af7b89bba761311654c622786f7b3326fa7ca97c6250514278a33977fc89b4ab890f353ca18f4d295bd36dcc034e27577f8abd0842e598a
|
data/bin/git-statistics
CHANGED
data/bin/git_statistics
CHANGED
data/lib/git_statistics.rb
CHANGED
@@ -4,11 +4,10 @@ module GitStatistics
|
|
4
4
|
class CLI
|
5
5
|
attr_reader :repository, :options
|
6
6
|
|
7
|
-
DEFAULT_BRANCH =
|
7
|
+
DEFAULT_BRANCH = 'master'
|
8
8
|
|
9
9
|
def initialize(dir)
|
10
|
-
|
11
|
-
@repository = Rugged::Repository.new(repository_location)
|
10
|
+
@repository = dir.nil? ? Rugged::Repository.discover(Dir.pwd) : Rugged::Repository.discover(dir)
|
12
11
|
@collected = false
|
13
12
|
@collector = nil
|
14
13
|
@options = OpenStruct.new(
|
@@ -16,7 +15,7 @@ module GitStatistics
|
|
16
15
|
merges: false,
|
17
16
|
pretty: false,
|
18
17
|
update: false,
|
19
|
-
sort:
|
18
|
+
sort: 'commits',
|
20
19
|
top: 0,
|
21
20
|
branch: DEFAULT_BRANCH,
|
22
21
|
verbose: false,
|
@@ -35,19 +34,19 @@ module GitStatistics
|
|
35
34
|
end
|
36
35
|
|
37
36
|
def collect_and_only_update
|
38
|
-
|
39
|
-
# Ensure commit directory is present
|
40
|
-
@collector = Collector.new(repository, options.limit, false, options.pretty)
|
41
|
-
commits_directory = repository.workdir + ".git_statistics/"
|
42
|
-
FileUtils.mkdir_p(commits_directory)
|
43
|
-
file_count = Utilities.number_of_matching_files(commits_directory, /\d+\.json/) - 1
|
37
|
+
return unless options.update
|
44
38
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
39
|
+
# Ensure commit directory is present
|
40
|
+
@collector = Collector.new(repository, options.limit, false, options.pretty)
|
41
|
+
commits_directory = repository.workdir + '.git_statistics/'
|
42
|
+
FileUtils.mkdir_p(commits_directory)
|
43
|
+
file_count = Utilities.number_of_matching_files(commits_directory, /\d+\.json/) - 1
|
44
|
+
|
45
|
+
return unless file_count >= 0
|
46
|
+
|
47
|
+
time_since = Utilities.get_modified_time(commits_directory + "#{file_count}.json").to_s
|
48
|
+
@collector.collect(branch: options.branch, time_since: time_since)
|
49
|
+
@collected = true
|
51
50
|
end
|
52
51
|
|
53
52
|
def calculate!
|
@@ -61,40 +60,40 @@ module GitStatistics
|
|
61
60
|
|
62
61
|
def fresh_collect!
|
63
62
|
@collector = Collector.new(repository, options.limit, true, options.pretty)
|
64
|
-
@collector.collect(
|
63
|
+
@collector.collect(branch: options.branch)
|
65
64
|
end
|
66
65
|
|
67
66
|
def parse_options
|
68
67
|
OptionParser.new do |opt|
|
69
68
|
opt.version = VERSION
|
70
|
-
opt.on
|
69
|
+
opt.on '-e', '--email', "Use author's email instead of name" do
|
71
70
|
options.email = true
|
72
71
|
end
|
73
|
-
opt.on
|
72
|
+
opt.on '-m', '--merges', 'Factor in merges when calculating statistics' do
|
74
73
|
options.merges = true
|
75
74
|
end
|
76
|
-
opt.on
|
75
|
+
opt.on '-p', '--pretty', 'Save the commits in git_repo/.git_statistics in pretty print (larger file size)' do
|
77
76
|
options.pretty = true
|
78
77
|
end
|
79
|
-
opt.on
|
78
|
+
opt.on '-u', '--update', 'Update saved commits with new data' do
|
80
79
|
options.update = true
|
81
80
|
end
|
82
|
-
opt.on
|
81
|
+
opt.on '-s', '--sort TYPE', 'Sort authors by {commits, additions, deletions, create, delete, rename, copy, merges}' do |type|
|
83
82
|
options.sort = type
|
84
83
|
end
|
85
|
-
opt.on
|
84
|
+
opt.on '-t', '--top N', Float, 'Show the top N authors in results' do |value|
|
86
85
|
options.top = value
|
87
86
|
end
|
88
|
-
opt.on
|
87
|
+
opt.on '-b', '--branch BRANCH', 'Use the specified branch for statistics (otherwise the master branch is used)' do |branch|
|
89
88
|
options.branch = branch
|
90
89
|
end
|
91
|
-
opt.on
|
90
|
+
opt.on '-v', '--verbose', 'Verbose output (shows INFO level log statements)' do
|
92
91
|
options.verbose = true
|
93
92
|
end
|
94
|
-
opt.on
|
93
|
+
opt.on '-d', '--debug', 'Debug output (shows DEBUG level log statements)' do
|
95
94
|
options.debug = true
|
96
95
|
end
|
97
|
-
opt.on
|
96
|
+
opt.on '-l', '--limit MAX_COMMITS', Float, 'The maximum limit of commits to hold in memory at a time' do |number|
|
98
97
|
options.limit = number
|
99
98
|
end
|
100
99
|
end.parse!
|
@@ -102,14 +101,13 @@ module GitStatistics
|
|
102
101
|
|
103
102
|
private
|
104
103
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
end
|
104
|
+
def determine_log_level
|
105
|
+
if options.debug
|
106
|
+
Log.level = Logger::DEBUG
|
107
|
+
Log.use_debug
|
108
|
+
elsif options.verbose
|
109
|
+
Log.level = Logger::INFO
|
112
110
|
end
|
113
|
-
|
111
|
+
end
|
114
112
|
end
|
115
113
|
end
|
@@ -1,41 +1,39 @@
|
|
1
1
|
module GitStatistics
|
2
2
|
class Collector
|
3
|
-
|
4
3
|
attr_accessor :repo, :commits_path, :commits
|
5
4
|
|
6
5
|
def initialize(repo, limit, fresh, pretty)
|
7
6
|
@repo = repo
|
8
|
-
@commits_path = repo.workdir +
|
7
|
+
@commits_path = repo.workdir + '.git_statistics'
|
9
8
|
@commits = Commits.new(@commits_path, fresh, limit, pretty)
|
10
9
|
end
|
11
10
|
|
12
11
|
def collect(options = {})
|
13
|
-
|
14
|
-
branch_head = Rugged::Branch.lookup(repo, branch).tip
|
12
|
+
branch_name = options[:branch] ? options[:branch] : CLI::DEFAULT_BRANCH
|
15
13
|
|
16
14
|
walker = Rugged::Walker.new(repo)
|
17
|
-
walker.push(
|
15
|
+
walker.push(repo.branches[branch_name].target)
|
18
16
|
|
19
17
|
walker.each_with_index do |commit, count|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
next unless valid_commit?(commit, options)
|
19
|
+
|
20
|
+
extract_commit(commit, count + 1)
|
21
|
+
@commits.flush_commits
|
24
22
|
end
|
25
23
|
|
26
24
|
@commits.flush_commits(true)
|
27
25
|
end
|
28
26
|
|
29
27
|
def valid_commit?(commit, options)
|
30
|
-
|
28
|
+
unless options[:time_since].nil?
|
31
29
|
return false unless commit.author[:time] > DateTime.parse(options[:time_since].to_s).to_time
|
32
30
|
end
|
33
31
|
|
34
|
-
|
32
|
+
unless options[:time_until].nil?
|
35
33
|
return false unless commit.author[:time] < DateTime.parse(options[:time_until].to_s).to_time
|
36
34
|
end
|
37
35
|
|
38
|
-
|
36
|
+
true
|
39
37
|
end
|
40
38
|
|
41
39
|
def acquire_commit_meta(commit_summary)
|
@@ -52,9 +50,9 @@ module GitStatistics
|
|
52
50
|
data[:added_files] = commit_summary.added_files
|
53
51
|
data[:deleted_files] = commit_summary.deleted_files
|
54
52
|
data[:modified_files] = commit_summary.modified_files
|
55
|
-
data[:files] = commit_summary.file_stats.map
|
53
|
+
data[:files] = commit_summary.file_stats.map(&:to_json)
|
56
54
|
|
57
|
-
|
55
|
+
data
|
58
56
|
end
|
59
57
|
|
60
58
|
def extract_commit(commit, count)
|
@@ -64,8 +62,7 @@ module GitStatistics
|
|
64
62
|
# Acquire meta information about commit
|
65
63
|
commit_data = acquire_commit_meta(commit_summary)
|
66
64
|
|
67
|
-
|
65
|
+
commit_data
|
68
66
|
end
|
69
|
-
|
70
67
|
end
|
71
68
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module GitStatistics
|
2
2
|
class CommitSummary < SimpleDelegator
|
3
|
+
attr_reader :patches
|
4
|
+
|
3
5
|
def initialize(repo, commit)
|
4
6
|
super(commit)
|
5
7
|
@repo = repo
|
@@ -14,17 +16,17 @@ module GitStatistics
|
|
14
16
|
|
15
17
|
# How many files were removed in this commit
|
16
18
|
def deleted_files
|
17
|
-
file_stats.
|
19
|
+
file_stats.count { |file| file.status == :deleted }
|
18
20
|
end
|
19
21
|
|
20
22
|
# How many files were added in this commit
|
21
23
|
def added_files
|
22
|
-
file_stats.
|
24
|
+
file_stats.count { |file| file.status == :added }
|
23
25
|
end
|
24
26
|
|
25
27
|
# How many files were modified (not added/deleted) in this commit
|
26
28
|
def modified_files
|
27
|
-
file_stats.
|
29
|
+
file_stats.count { |file| file.status == :modified }
|
28
30
|
end
|
29
31
|
|
30
32
|
# How many total additions in this commit?
|
@@ -43,14 +45,14 @@ module GitStatistics
|
|
43
45
|
end
|
44
46
|
|
45
47
|
def file_stats
|
46
|
-
@cached_file_stats ||=
|
48
|
+
@cached_file_stats ||= @patches.map { |diff| DiffSummary.new(@repo, diff) }
|
47
49
|
end
|
48
50
|
|
49
51
|
LanguageSummary = Struct.new(:name, :additions, :deletions, :net, :added_files, :deleted_files, :modified_files)
|
50
52
|
|
51
53
|
# Array of LanguageSummary objects (one for each language) for simple calculations
|
52
54
|
def languages
|
53
|
-
grouped_language_files.
|
55
|
+
grouped_language_files.map do |language, stats|
|
54
56
|
additions = summarize(stats, :additions)
|
55
57
|
deletions = summarize(stats, :deletions)
|
56
58
|
net = summarize(stats, :net)
|
@@ -70,17 +72,12 @@ module GitStatistics
|
|
70
72
|
|
71
73
|
private
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
def commit_summary(what)
|
78
|
-
summarize(file_stats, what)
|
79
|
-
end
|
80
|
-
|
81
|
-
def diffstats
|
82
|
-
@patches
|
83
|
-
end
|
75
|
+
def summarize(stats, what)
|
76
|
+
stats.map(&what).inject(0, :+)
|
77
|
+
end
|
84
78
|
|
79
|
+
def commit_summary(what)
|
80
|
+
summarize(file_stats, what)
|
81
|
+
end
|
85
82
|
end
|
86
83
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module GitStatistics
|
2
2
|
class Commits < Hash
|
3
|
-
|
4
3
|
attr_accessor :stats, :totals, :path, :fresh, :limit, :pretty
|
5
4
|
|
6
5
|
def initialize(path, fresh, limit, pretty)
|
@@ -30,23 +29,23 @@ module GitStatistics
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def files_in_path
|
33
|
-
Dir.entries(path).reject { |file| %w
|
32
|
+
Dir.entries(path).reject { |file| %w(. ..).include?(file) }
|
34
33
|
end
|
35
34
|
|
36
|
-
def flush_commits(force=false)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
def flush_commits(force = false)
|
36
|
+
return unless size >= limit || force
|
37
|
+
|
38
|
+
file_count = Utilities.number_of_matching_files(path, /\d+\.json/)
|
39
|
+
save(File.join(path, "#{file_count}.json"), @pretty)
|
40
|
+
clear
|
42
41
|
end
|
43
42
|
|
44
43
|
def author_top_n_type(type, top_n = 0)
|
45
44
|
top_n = 0 if top_n < 0
|
46
|
-
if @stats.empty? || !@stats.first[1].
|
45
|
+
if @stats.empty? || !@stats.first[1].key?(type)
|
47
46
|
nil
|
48
47
|
else
|
49
|
-
Hash[*@stats.sorted_hash {|a,b| b[1][type.to_sym] <=> a[1][type]}.to_a[0..top_n-1].flatten]
|
48
|
+
Hash[*@stats.sorted_hash { |a, b| b[1][type.to_sym] <=> a[1][type] }.to_a[0..top_n - 1].flatten]
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
@@ -55,24 +54,24 @@ module GitStatistics
|
|
55
54
|
type = email ? :author_email : :author
|
56
55
|
|
57
56
|
# Process the commits from file or memory
|
58
|
-
files = Dir.entries(path) - [
|
57
|
+
files = Dir.entries(path) - ['.', '..']
|
59
58
|
if files.size == 0
|
60
|
-
|
59
|
+
process_commits(type, merge)
|
61
60
|
else
|
62
|
-
#Load commit file and extract the commits
|
61
|
+
# Load commit file and extract the commits
|
63
62
|
files.each do |file|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
63
|
+
next unless file =~ /\d+\.json/
|
64
|
+
|
65
|
+
load(File.join(path, file))
|
66
|
+
process_commits(type, merge)
|
67
|
+
clear
|
69
68
|
end
|
70
69
|
end
|
71
70
|
end
|
72
71
|
|
73
72
|
def process_commits(type, merge)
|
74
73
|
# Collect the stats from each commit
|
75
|
-
each do |key,value|
|
74
|
+
each do |key, value|
|
76
75
|
next if !merge && value[:merge]
|
77
76
|
# If there are no changed files move to next commit
|
78
77
|
next if value[:files].empty?
|
@@ -88,7 +87,6 @@ module GitStatistics
|
|
88
87
|
|
89
88
|
# Collect language stats
|
90
89
|
value[:files].each do |file|
|
91
|
-
|
92
90
|
# Add to author's languages
|
93
91
|
add_language_stats(author, file)
|
94
92
|
|
@@ -118,11 +116,11 @@ module GitStatistics
|
|
118
116
|
data[:languages][file[:language].to_sym][:deletions] += file[:deletions]
|
119
117
|
|
120
118
|
# Keep count of languages status (i.e., added, deleted) and keep keys consistent (i.e., added_files, deleted_files)
|
121
|
-
|
122
|
-
data[:languages][file[:language].to_sym][(file[:status]+'_files').to_sym] += 1
|
119
|
+
unless file[:status].nil?
|
120
|
+
data[:languages][file[:language].to_sym][(file[:status] + '_files').to_sym] += 1
|
123
121
|
end
|
124
122
|
|
125
|
-
|
123
|
+
data
|
126
124
|
end
|
127
125
|
|
128
126
|
def add_commit_stats(data, commit)
|
@@ -131,25 +129,25 @@ module GitStatistics
|
|
131
129
|
data[:commits] += 1
|
132
130
|
data[:additions] += commit[:additions]
|
133
131
|
data[:deletions] += commit[:deletions]
|
134
|
-
data[:added_files] += commit[:added_files]
|
135
|
-
data[:deleted_files] += commit[:deleted_files]
|
136
|
-
|
132
|
+
data[:added_files] += commit[:added_files] unless commit[:added_files].nil?
|
133
|
+
data[:deleted_files] += commit[:deleted_files] unless commit[:deleted_files].nil?
|
134
|
+
data
|
137
135
|
end
|
138
136
|
|
139
137
|
def load(file)
|
140
|
-
merge!(JSON.parse(File.read(file), :
|
138
|
+
merge!(JSON.parse(File.read(file), symbolize_names: true))
|
141
139
|
end
|
142
140
|
|
143
141
|
def save(file, pretty)
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
142
|
+
return if empty?
|
143
|
+
|
144
|
+
# Ensure the path to the file exists
|
145
|
+
FileUtils.mkdir_p(File.dirname(file))
|
146
|
+
|
147
|
+
# Save file in a simple or pretty format
|
148
|
+
File.open(file, 'w') do |f|
|
149
|
+
json_content = pretty ? JSON.pretty_generate(self) : to_json
|
150
|
+
f.write(json_content)
|
153
151
|
end
|
154
152
|
end
|
155
153
|
end
|
@@ -35,7 +35,7 @@ module GitStatistics
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def filename
|
38
|
-
if
|
38
|
+
if status == :deleted
|
39
39
|
delta.old_file[:path]
|
40
40
|
else
|
41
41
|
delta.new_file[:path]
|
@@ -44,34 +44,30 @@ module GitStatistics
|
|
44
44
|
|
45
45
|
# We flip these around since we are diffing in the opposite direction -- new.diff(old)
|
46
46
|
def blob
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
blob = @repo.lookup(delta.old_file[:oid]) # Look at old instead of new
|
52
|
-
end
|
53
|
-
rescue Rugged::OdbError
|
54
|
-
Log.warn "Could not find object (most likely a submodule)"
|
55
|
-
blob = nil
|
47
|
+
if status == :deleted
|
48
|
+
@repo.lookup(delta.new_file[:oid]) # Look at new instead of old
|
49
|
+
else
|
50
|
+
@repo.lookup(delta.old_file[:oid]) # Look at old instead of new
|
56
51
|
end
|
52
|
+
rescue Rugged::OdbError
|
53
|
+
Log.warn 'Could not find object (most likely a submodule)'
|
54
|
+
nil
|
57
55
|
end
|
58
56
|
|
59
57
|
def inspect
|
60
|
-
|
58
|
+
"<GitStatistics::FileStat @filename=#{filename} @status=#{status} @similarity=#{similarity} @language=#{language} @additions=#{additions}, @deletions=#{deletions}, @net=#{net}>"
|
61
59
|
end
|
62
60
|
|
63
61
|
def to_json
|
64
|
-
{ filename: filename, status: status, similarity: similarity, language: language, additions: additions, deletions: deletions, net: net}
|
62
|
+
{ filename: filename, status: status, similarity: similarity, language: language, additions: additions, deletions: deletions, net: net }
|
65
63
|
end
|
66
64
|
|
67
65
|
# Determine the language of the file from the blob
|
68
66
|
def language
|
69
|
-
language =
|
67
|
+
language = 'Unknown'
|
70
68
|
unless blob.nil?
|
71
|
-
detected_language = LanguageSniffer.detect(filename, :
|
72
|
-
unless detected_language.nil?
|
73
|
-
language = detected_language.name
|
74
|
-
end
|
69
|
+
detected_language = LanguageSniffer.detect(filename, content: blob.content).language
|
70
|
+
language = detected_language.name unless detected_language.nil?
|
75
71
|
end
|
76
72
|
language
|
77
73
|
end
|