git_statistics 0.7.0 → 0.8.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.
- 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
|