git_statistics 0.6.0 → 0.7.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 +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
|