git_statistics 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/git-statistics +1 -1
- data/bin/git_statistics +1 -1
- data/lib/git_statistics.rb +48 -32
- data/lib/git_statistics/collector.rb +40 -187
- data/lib/git_statistics/commit_summary.rb +86 -0
- data/lib/git_statistics/commits.rb +18 -14
- data/lib/git_statistics/diff_summary.rb +79 -0
- data/lib/git_statistics/formatters/console.rb +31 -26
- data/lib/git_statistics/initialize.rb +7 -6
- data/lib/git_statistics/pipe.rb +1 -1
- data/lib/git_statistics/utilities.rb +28 -91
- data/lib/git_statistics/version.rb +1 -1
- data/spec/collector_spec.rb +41 -235
- data/spec/commit_summary_spec.rb +126 -0
- data/spec/commits_spec.rb +52 -58
- data/spec/formatters/console_spec.rb +11 -10
- data/spec/utilities_spec.rb +15 -128
- metadata +22 -75
- data/lib/git_statistics/blob.rb +0 -5
- data/lib/git_statistics/branches.rb +0 -35
- data/lib/git_statistics/commit_line_extractor.rb +0 -50
- data/lib/git_statistics/regex_matcher.rb +0 -23
- data/spec/branches_spec.rb +0 -44
- data/spec/commit_line_extractor_spec.rb +0 -70
- data/spec/regex_matcher_spec.rb +0 -35
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ac2c8d9e4ff4611dfa6c6b2f9f664fb874a7201f
|
4
|
+
data.tar.gz: 9da9f644f174806900bd8b2a2921906bfc3e3989
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 381f1532eee3178c8a1459ec2f5ca37f1429786c56e4bae54c6139e2e9ed4fea6253fc4960d6b426e360e777c6adad1c48bffac53b1ce4fe9f93b5de27f5e51f
|
7
|
+
data.tar.gz: b7bb4f77e58b6f1bf73cfd09219651c0256a59a8a9b3de25a50c11948063a66c3df88856b14210709a6353f1fd0bf97d5eeaafea95f3d36d0a7587bf4d5fd7cf
|
data/bin/git-statistics
CHANGED
data/bin/git_statistics
CHANGED
data/lib/git_statistics.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
require 'optparse'
|
3
1
|
require 'git_statistics/initialize'
|
4
2
|
|
5
3
|
module GitStatistics
|
6
|
-
class
|
7
|
-
attr_reader :options
|
8
|
-
|
4
|
+
class CLI
|
5
|
+
attr_reader :repository, :options
|
6
|
+
|
7
|
+
DEFAULT_BRANCH = "master"
|
8
|
+
|
9
|
+
def initialize(dir)
|
10
|
+
repository_location = dir.nil? ? Rugged::Repository.discover(Dir.pwd) : Rugged::Repository.discover(dir)
|
11
|
+
@repository = Rugged::Repository.new(repository_location)
|
12
|
+
@collected = false
|
13
|
+
@collector = nil
|
9
14
|
@options = OpenStruct.new(
|
10
15
|
email: false,
|
11
16
|
merges: false,
|
@@ -13,7 +18,7 @@ module GitStatistics
|
|
13
18
|
update: false,
|
14
19
|
sort: "commits",
|
15
20
|
top: 0,
|
16
|
-
branch:
|
21
|
+
branch: DEFAULT_BRANCH,
|
17
22
|
verbose: false,
|
18
23
|
debug: false,
|
19
24
|
limit: 100
|
@@ -22,43 +27,43 @@ module GitStatistics
|
|
22
27
|
end
|
23
28
|
|
24
29
|
def execute
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
determine_log_level
|
31
|
+
collect_and_only_update
|
32
|
+
fresh_collect! unless @collected
|
33
|
+
calculate!
|
34
|
+
output_results
|
35
|
+
end
|
31
36
|
|
32
|
-
|
37
|
+
def collect_and_only_update
|
33
38
|
if options.update
|
34
39
|
# Ensure commit directory is present
|
35
|
-
collector = Collector.new(options.limit, false, options.pretty)
|
36
|
-
commits_directory =
|
40
|
+
@collector = Collector.new(repository, options.limit, false, options.pretty)
|
41
|
+
commits_directory = repository.workdir + ".git_statistics/"
|
37
42
|
FileUtils.mkdir_p(commits_directory)
|
38
43
|
file_count = Utilities.number_of_matching_files(commits_directory, /\d+\.json/) - 1
|
39
44
|
|
40
45
|
if file_count >= 0
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
collected = true
|
46
|
+
time_since = Utilities.get_modified_time(commits_directory + "#{file_count}.json").to_s
|
47
|
+
@collector.collect({:branch => options.branch, :time_since => time_since})
|
48
|
+
@collected = true
|
45
49
|
end
|
46
50
|
end
|
51
|
+
end
|
47
52
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
collector.collect(options.branch)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Calculate statistics
|
55
|
-
collector.commits.calculate_statistics(options.email, options.merges)
|
53
|
+
def calculate!
|
54
|
+
@collector.commits.calculate_statistics(options.email, options.merges)
|
55
|
+
end
|
56
56
|
|
57
|
-
|
58
|
-
results = Formatters::Console.new(collector.commits)
|
57
|
+
def output_results
|
58
|
+
results = Formatters::Console.new(@collector.commits)
|
59
59
|
puts results.print_summary(options.sort, options.email, options.top)
|
60
60
|
end
|
61
61
|
|
62
|
+
def fresh_collect!
|
63
|
+
@collector = Collector.new(repository, options.limit, true, options.pretty)
|
64
|
+
@collector.collect({:branch => options.branch})
|
65
|
+
end
|
66
|
+
|
62
67
|
def parse_options
|
63
68
|
OptionParser.new do |opt|
|
64
69
|
opt.version = VERSION
|
@@ -80,8 +85,8 @@ module GitStatistics
|
|
80
85
|
opt.on "-t", "--top N", Float,"Show the top N authors in results" do |value|
|
81
86
|
options.top = value
|
82
87
|
end
|
83
|
-
opt.on "-b", "--branch", "Use
|
84
|
-
options.branch =
|
88
|
+
opt.on "-b", "--branch BRANCH", "Use the specified branch for statistics (otherwise the master branch is used)" do |branch|
|
89
|
+
options.branch = branch
|
85
90
|
end
|
86
91
|
opt.on "-v", "--verbose", "Verbose output (shows INFO level log statements)" do
|
87
92
|
options.verbose = true
|
@@ -94,6 +99,17 @@ module GitStatistics
|
|
94
99
|
end
|
95
100
|
end.parse!
|
96
101
|
end
|
97
|
-
end
|
98
102
|
|
103
|
+
private
|
104
|
+
|
105
|
+
def determine_log_level
|
106
|
+
if options.debug
|
107
|
+
Log.level = Logger::DEBUG
|
108
|
+
Log.use_debug
|
109
|
+
elsif options.verbose
|
110
|
+
Log.level = Logger::INFO
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
99
115
|
end
|
@@ -1,217 +1,70 @@
|
|
1
1
|
module GitStatistics
|
2
2
|
class Collector
|
3
3
|
|
4
|
-
attr_accessor :repo, :
|
4
|
+
attr_accessor :repo, :commits_path, :commits
|
5
5
|
|
6
|
-
def initialize(limit, fresh, pretty)
|
7
|
-
@repo =
|
8
|
-
@
|
9
|
-
@commits_path = File.join(@repo_path, ".git_statistics")
|
6
|
+
def initialize(repo, limit, fresh, pretty)
|
7
|
+
@repo = repo
|
8
|
+
@commits_path = repo.workdir + ".git_statistics"
|
10
9
|
@commits = Commits.new(@commits_path, fresh, limit, pretty)
|
11
10
|
end
|
12
11
|
|
13
|
-
def collect(
|
14
|
-
|
15
|
-
|
12
|
+
def collect(options = {})
|
13
|
+
branch = options[:branch] ? options[:branch] : CLI::DEFAULT_BRANCH
|
14
|
+
branch_head = Rugged::Branch.lookup(repo, branch).tip
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
" --no-color --find-copies-harder --numstat --encoding=utf-8"\
|
20
|
-
" --summary #{time_since} #{time_until}"\
|
21
|
-
" --format=\"%H,%an,%ae,%ad,%p\"")
|
16
|
+
walker = Rugged::Walker.new(repo)
|
17
|
+
walker.push(branch_head)
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# Extract the buffer (commit) when we match ','x5 in the log format (delimeter)
|
28
|
-
if line.split(',').size == 5
|
29
|
-
|
30
|
-
# Sometimes 'git log' doesn't populate the buffer (i.e., merges), try fallback option if so
|
31
|
-
buffer = fall_back_collect_commit(buffer[0].split(',').first) if buffer.one?
|
32
|
-
|
33
|
-
extract_commit(buffer) unless buffer.empty?
|
34
|
-
buffer = []
|
35
|
-
|
36
|
-
# Save commits to file if size exceeds limit or forced
|
19
|
+
walker.each_with_index do |commit, count|
|
20
|
+
if valid_commit?(commit, options)
|
21
|
+
extract_commit(commit, count + 1)
|
37
22
|
@commits.flush_commits
|
38
23
|
end
|
39
|
-
|
40
|
-
buffer << line
|
41
24
|
end
|
42
25
|
|
43
|
-
# Extract the last commit
|
44
|
-
extract_commit(buffer) unless buffer.empty?
|
45
26
|
@commits.flush_commits(true)
|
46
27
|
end
|
47
28
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
" --no-color --find-copies-harder --numstat --encoding=utf-8 "\
|
52
|
-
"--summary --format=\"%H,%an,%ae,%ad,%p\"")
|
53
|
-
|
54
|
-
# Check that the buffer has valid information (i.e., sha was valid)
|
55
|
-
if !pipe.empty? && pipe.first.split(',').first == sha
|
56
|
-
pipe.to_a
|
57
|
-
else
|
58
|
-
[]
|
29
|
+
def valid_commit?(commit, options)
|
30
|
+
if !options[:time_since].nil?
|
31
|
+
return false unless commit.author[:time] > DateTime.parse(options[:time_since].to_s).to_time
|
59
32
|
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def acquire_commit_data(line)
|
63
|
-
# Split up formated line
|
64
|
-
commit_info = line.split(',')
|
65
33
|
|
66
|
-
|
67
|
-
|
68
|
-
data[:author] = commit_info[1]
|
69
|
-
data[:author_email] = commit_info[2]
|
70
|
-
data[:time] = commit_info[3]
|
71
|
-
data[:files] = []
|
72
|
-
|
73
|
-
# Flag commit as merge if necessary (determined if two parents)
|
74
|
-
if commit_info[4].nil? || commit_info[4].split(' ').one?
|
75
|
-
data[:merge] = false
|
76
|
-
else
|
77
|
-
data[:merge] = true
|
34
|
+
if !options[:time_until].nil?
|
35
|
+
return false unless commit.author[:time] < DateTime.parse(options[:time_until].to_s).to_time
|
78
36
|
end
|
79
37
|
|
80
|
-
return
|
38
|
+
return true
|
81
39
|
end
|
82
40
|
|
83
|
-
def
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
# No files were changed in this commit, abort commit
|
99
|
-
if files.nil?
|
100
|
-
Log.debug "No files were changed"
|
101
|
-
return
|
102
|
-
end
|
103
|
-
|
104
|
-
# Acquire blob for each changed file and process it
|
105
|
-
files.each do |file|
|
106
|
-
blob = get_blob(commit_data[:sha], file)
|
107
|
-
|
108
|
-
# Only process blobs, or log the submodules and problematic files
|
109
|
-
if blob.instance_of?(Grit::Blob)
|
110
|
-
process_blob(commit_data[:data], blob, file)
|
111
|
-
elsif blob.instance_of?(Grit::Submodule)
|
112
|
-
Log.debug "Ignoring submodule #{blob.name}"
|
113
|
-
else
|
114
|
-
Log.warn "Problem processing file #{file[:file]}"
|
115
|
-
end
|
116
|
-
end
|
117
|
-
return commit_data[:data]
|
118
|
-
end
|
119
|
-
|
120
|
-
def get_blob(sha, file)
|
121
|
-
# Split up file for Grit navigation
|
122
|
-
file = file[:file].split(File::Separator)
|
123
|
-
|
124
|
-
# Acquire blob of the file for this specific commit
|
125
|
-
blob = Utilities.find_blob_in_tree(@repo.tree(sha), file)
|
126
|
-
|
127
|
-
# If we cannot find blob in current commit (deleted file), check previous commit
|
128
|
-
if blob.nil? || blob.instance_of?(Grit::Tree)
|
129
|
-
prev_commit = @repo.commits(sha).first.parents[0]
|
130
|
-
return nil if prev_commit.nil?
|
41
|
+
def acquire_commit_meta(commit_summary)
|
42
|
+
# Initialize commit data
|
43
|
+
data = (@commits[commit_summary.oid] ||= Hash.new(0))
|
44
|
+
|
45
|
+
data[:author] = commit_summary.author[:name]
|
46
|
+
data[:author_email] = commit_summary.author[:email]
|
47
|
+
data[:time] = commit_summary.author[:time].to_s
|
48
|
+
data[:merge] = commit_summary.merge?
|
49
|
+
data[:additions] = commit_summary.additions
|
50
|
+
data[:deletions] = commit_summary.deletions
|
51
|
+
data[:net] = commit_summary.net
|
52
|
+
data[:added_files] = commit_summary.added_files
|
53
|
+
data[:deleted_files] = commit_summary.deleted_files
|
54
|
+
data[:modified_files] = commit_summary.modified_files
|
55
|
+
data[:files] = commit_summary.file_stats.map{ |file| file.to_json }
|
131
56
|
|
132
|
-
|
133
|
-
blob = Utilities.find_blob_in_tree(prev_tree, file)
|
134
|
-
end
|
135
|
-
return blob
|
57
|
+
return data
|
136
58
|
end
|
137
59
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
141
|
-
# For each modification extract the details
|
142
|
-
changed_files = []
|
143
|
-
buffer.each do |line|
|
144
|
-
extracted_line = CommitLineExtractor.new(line)
|
60
|
+
def extract_commit(commit, count)
|
61
|
+
Log.info "Extracting(#{count}) #{commit.oid}"
|
62
|
+
commit_summary = CommitSummary.new(@repo, commit)
|
145
63
|
|
146
|
-
|
147
|
-
|
148
|
-
if changed_file_information.any?
|
149
|
-
changed_files << changed_file_information
|
150
|
-
next # This line is processed, skip to next
|
151
|
-
end
|
64
|
+
# Acquire meta information about commit
|
65
|
+
commit_data = acquire_commit_meta(commit_summary)
|
152
66
|
|
153
|
-
|
154
|
-
created_or_deleted = extracted_line.created_or_deleted
|
155
|
-
if created_or_deleted.any?
|
156
|
-
augmented = false
|
157
|
-
# Augment changed file with create/delete information if possible
|
158
|
-
changed_files.each do |file|
|
159
|
-
if file[:file] == created_or_deleted[:file]
|
160
|
-
file[:status] = created_or_deleted[:status]
|
161
|
-
augmented = true
|
162
|
-
break
|
163
|
-
end
|
164
|
-
end
|
165
|
-
changed_files << created_or_deleted unless augmented
|
166
|
-
next # This line is processed, skip to next
|
167
|
-
end
|
168
|
-
|
169
|
-
# Extract details of rename/copy files if it exists
|
170
|
-
renamed_or_copied = extracted_line.renamed_or_copied
|
171
|
-
if renamed_or_copied.any?
|
172
|
-
augmented = false
|
173
|
-
# Augment changed file with rename/copy information if possible
|
174
|
-
changed_files.each do |file|
|
175
|
-
if file[:file] == renamed_or_copied[:new_file]
|
176
|
-
file[:status] = renamed_or_copied[:status]
|
177
|
-
file[:old_file] = renamed_or_copied[:old_file]
|
178
|
-
file[:similar] = renamed_or_copied[:similar]
|
179
|
-
augmented = true
|
180
|
-
break
|
181
|
-
end
|
182
|
-
end
|
183
|
-
changed_files << renamed_or_copied unless augmented
|
184
|
-
next # This line is processed, skip to next
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
changed_files
|
189
|
-
end
|
190
|
-
|
191
|
-
def process_blob(data, blob, file)
|
192
|
-
# Initialize a hash to hold information regarding the file
|
193
|
-
file_hash = Hash.new(0)
|
194
|
-
file_hash[:name] = file[:file]
|
195
|
-
file_hash[:additions] = file[:additions]
|
196
|
-
file_hash[:deletions] = file[:deletions]
|
197
|
-
file_hash[:status] = file[:status]
|
198
|
-
|
199
|
-
# Add file information to commit itself
|
200
|
-
data[file[:status].to_sym] += 1 if file[:status] != nil
|
201
|
-
data[:additions] += file[:additions]
|
202
|
-
data[:deletions] += file[:deletions]
|
203
|
-
|
204
|
-
# Acquire specifics on blob
|
205
|
-
file_hash[:binary] = blob.binary?
|
206
|
-
file_hash[:image] = blob.image?
|
207
|
-
file_hash[:vendored] = blob.vendored?
|
208
|
-
file_hash[:generated] = blob.generated?
|
209
|
-
|
210
|
-
# Identify the language of the blob if possible
|
211
|
-
file_hash[:language] = blob.language.nil? ? "Unknown" : blob.language.name
|
212
|
-
data[:files] << file_hash
|
213
|
-
|
214
|
-
return data
|
67
|
+
return commit_data
|
215
68
|
end
|
216
69
|
|
217
70
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module GitStatistics
|
2
|
+
class CommitSummary < SimpleDelegator
|
3
|
+
def initialize(repo, commit)
|
4
|
+
super(commit)
|
5
|
+
@repo = repo
|
6
|
+
@diff = diff(commit.parents.first)
|
7
|
+
@patches = @diff.patches
|
8
|
+
end
|
9
|
+
|
10
|
+
# A Git commit is a merge if it has more than one parent
|
11
|
+
def merge?
|
12
|
+
parents.size > 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# How many files were removed in this commit
|
16
|
+
def deleted_files
|
17
|
+
file_stats.select { |file| file.status == :deleted }.count
|
18
|
+
end
|
19
|
+
|
20
|
+
# How many files were added in this commit
|
21
|
+
def added_files
|
22
|
+
file_stats.select { |file| file.status == :added }.count
|
23
|
+
end
|
24
|
+
|
25
|
+
# How many files were modified (not added/deleted) in this commit
|
26
|
+
def modified_files
|
27
|
+
file_stats.select { |file| file.status == :modified }.count
|
28
|
+
end
|
29
|
+
|
30
|
+
# How many total additions in this commit?
|
31
|
+
def additions
|
32
|
+
commit_summary(:additions)
|
33
|
+
end
|
34
|
+
|
35
|
+
# How many total deletions in this commit?
|
36
|
+
def deletions
|
37
|
+
commit_summary(:deletions)
|
38
|
+
end
|
39
|
+
|
40
|
+
# What is the net # of lines changes in this commit?
|
41
|
+
def net
|
42
|
+
commit_summary(:net)
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_stats
|
46
|
+
@cached_file_stats ||= diffstats.map { |diff| DiffSummary.new(@repo, diff) }
|
47
|
+
end
|
48
|
+
|
49
|
+
LanguageSummary = Struct.new(:name, :additions, :deletions, :net, :added_files, :deleted_files, :modified_files)
|
50
|
+
|
51
|
+
# Array of LanguageSummary objects (one for each language) for simple calculations
|
52
|
+
def languages
|
53
|
+
grouped_language_files.collect do |language, stats|
|
54
|
+
additions = summarize(stats, :additions)
|
55
|
+
deletions = summarize(stats, :deletions)
|
56
|
+
net = summarize(stats, :net)
|
57
|
+
LanguageSummary.new(language, additions, deletions, net, added_files, deleted_files, modified_files)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Group file statistics by language
|
62
|
+
def grouped_language_files
|
63
|
+
file_stats.group_by(&:language)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Files touched in this commit
|
67
|
+
def filenames
|
68
|
+
file_stats.map(&:filename)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def summarize(stats, what)
|
74
|
+
stats.map(&what).inject(0, :+)
|
75
|
+
end
|
76
|
+
|
77
|
+
def commit_summary(what)
|
78
|
+
summarize(file_stats, what)
|
79
|
+
end
|
80
|
+
|
81
|
+
def diffstats
|
82
|
+
@patches
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|