git-commits-analyzer 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/analyze_commits +22 -15
- data/lib/git-commits-analyzer/utils.rb +14 -6
- data/lib/git-commits-analyzer.rb +74 -21
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3235fa842b69838ddc164316ad8394c08f71b6a5
|
4
|
+
data.tar.gz: 6bb686c2baf86c75abc8d31f752c4968fd1931ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7f5f16a6b4b26bcae93456c26d7df31de2fb3b6a35c74b5b268184f529c5303a81b6262a60a6da8d3075a9d901d8c13ef930285bb705978ee836de3ffb296a5
|
7
|
+
data.tar.gz: 82f66448cfe0081c0881cb302dee08c080938e7778a6762fb8119e39143cec4145161412de4c511a5489ca6e1b3f1b1baf64c53aa44eddf368b71fc5c0ab7939
|
data/bin/analyze_commits
CHANGED
@@ -23,29 +23,36 @@ options = Utils.parse_command_line_options()
|
|
23
23
|
|
24
24
|
# Find git repos to inspect.
|
25
25
|
repos = Utils.get_git_repos(path: options[:path])
|
26
|
-
|
27
|
-
puts
|
26
|
+
printf("Found %s repos to inspect.\n", repos.length.to_s)
|
27
|
+
puts ''
|
28
28
|
|
29
29
|
# Inspect git repos.
|
30
|
-
puts
|
31
|
-
puts
|
32
|
-
git_commits_analyzer = GitCommitsAnalyzer.new(
|
30
|
+
puts '===== Inspecting repos ====='
|
31
|
+
puts ''
|
32
|
+
git_commits_analyzer = GitCommitsAnalyzer.new(
|
33
|
+
logger: logger,
|
34
|
+
author: options[:authors]
|
35
|
+
)
|
33
36
|
repos.sort.each do |repo|
|
34
|
-
|
37
|
+
printf("Inspecting repo %s.\n", repo)
|
35
38
|
git_commits_analyzer.parse_repo(repo: repo)
|
36
|
-
#break
|
39
|
+
# break
|
37
40
|
end
|
38
|
-
puts
|
41
|
+
puts ''
|
39
42
|
|
40
43
|
# Display sanity check.
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
+
printf(
|
45
|
+
"Found %s commits for author(s) %s.\n",
|
46
|
+
git_commits_analyzer.commits_total,
|
47
|
+
options[:authors].join(', ')
|
48
|
+
)
|
49
|
+
puts ''
|
50
|
+
exit if git_commits_analyzer.commits_by_month.keys.empty?
|
44
51
|
|
45
52
|
# Save data.
|
46
|
-
puts
|
47
|
-
puts
|
48
|
-
output_file = options[:output]
|
53
|
+
puts '===== Save data ====='
|
54
|
+
puts ''
|
55
|
+
output_file = options[:output]
|
49
56
|
File.open(output_file, 'w') { |file| file.write(git_commits_analyzer.to_json) }
|
50
57
|
puts "Re-generated #{output_file}."
|
51
|
-
puts
|
58
|
+
puts ''
|
@@ -1,36 +1,44 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
|
+
# Public: supporting functions for the command-line utility.
|
4
|
+
#
|
5
|
+
# Examples:
|
6
|
+
#
|
7
|
+
# require 'git-commits-analyzer/utils'
|
8
|
+
# options = Utils.parse_command_line_options()
|
9
|
+
# repos = Utils.get_git_repos(path)
|
10
|
+
#
|
3
11
|
class Utils
|
4
12
|
def self.parse_command_line_options()
|
5
13
|
options = {}
|
6
14
|
OptionParser.new do |opts|
|
7
|
-
opts.banner =
|
15
|
+
opts.banner = 'Usage: inspect_contributions.rb [options]'
|
8
16
|
options[:authors] = []
|
9
17
|
|
10
18
|
# Parse path.
|
11
|
-
opts.on(
|
19
|
+
opts.on('-p', '--path PATH', 'Specify a path to search for git repositories under') do |path|
|
12
20
|
options[:path] = path
|
13
21
|
end
|
14
22
|
|
15
23
|
# Parse authors.
|
16
|
-
opts.on(
|
24
|
+
opts.on('-a', '--author EMAIL', 'Include this author in statistics') do |email|
|
17
25
|
options[:authors] << email
|
18
26
|
end
|
19
27
|
|
20
28
|
# Parse output directory.
|
21
|
-
opts.on(
|
29
|
+
opts.on('-p', '--output PATH', 'Specify a path to output files with collected data') do |output|
|
22
30
|
options[:output] = output
|
23
31
|
end
|
24
32
|
|
25
33
|
# Show usage
|
26
|
-
opts.on_tail(
|
34
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
27
35
|
puts opts
|
28
36
|
exit
|
29
37
|
end
|
30
38
|
end.parse!
|
31
39
|
|
32
40
|
# Check mandatory options.
|
33
|
-
raise OptionParser::MissingArgument, '--author' if options[:authors].
|
41
|
+
raise OptionParser::MissingArgument, '--author' if options[:authors].empty?
|
34
42
|
raise OptionParser::MissingArgument, '--output' if options[:output].nil?
|
35
43
|
raise OptionParser::MissingArgument, '--path' if options[:path].nil?
|
36
44
|
|
data/lib/git-commits-analyzer.rb
CHANGED
@@ -11,15 +11,24 @@ require 'json'
|
|
11
11
|
#
|
12
12
|
class GitCommitsAnalyzer
|
13
13
|
# Public: Returns a hash of commit numbers broken down by month.
|
14
|
-
attr_reader :
|
14
|
+
attr_reader :commits_by_month
|
15
15
|
|
16
16
|
# Public: Returns the total number of commits belonging to the author
|
17
17
|
# specified.
|
18
|
-
attr_reader :
|
18
|
+
attr_reader :commits_total
|
19
19
|
|
20
20
|
# Public: Returns the number of lines added/removed broken down by language.
|
21
21
|
attr_reader :lines_by_language
|
22
22
|
|
23
|
+
# Public: Returns the tally of commits broken down by hour of the day.
|
24
|
+
attr_reader :commit_hours
|
25
|
+
|
26
|
+
# Public: Returns the tally of commits broken down by day.
|
27
|
+
attr_reader :commit_days
|
28
|
+
|
29
|
+
# Public: Returns the tally of commits broken down by weekday and hour.
|
30
|
+
attr_reader :commit_weekdays_hours
|
31
|
+
|
23
32
|
# Public: Initialize new GitParser object.
|
24
33
|
#
|
25
34
|
# logger - A logger object to display git errors/warnings.
|
@@ -28,10 +37,20 @@ class GitCommitsAnalyzer
|
|
28
37
|
def initialize(logger:, author:)
|
29
38
|
@logger = logger
|
30
39
|
@author = author
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@
|
40
|
+
@commits_by_month = {}
|
41
|
+
@commits_by_month.default = 0
|
42
|
+
@commits_total = 0
|
34
43
|
@lines_by_language = {}
|
44
|
+
@commit_hours = 0.upto(23).map{ |x| [x, 0] }.to_h
|
45
|
+
@commit_days = {}
|
46
|
+
@commit_days.default = 0
|
47
|
+
@commit_weekdays_hours = {}
|
48
|
+
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].each do |weekday|
|
49
|
+
@commit_weekdays_hours[weekday] = {}
|
50
|
+
0.upto(23).each do |hour|
|
51
|
+
@commit_weekdays_hours[weekday][hour] = 0
|
52
|
+
end
|
53
|
+
end
|
35
54
|
end
|
36
55
|
|
37
56
|
# Public: Determine the type of a file at the given revision of a repo.
|
@@ -49,7 +68,9 @@ class GitCommitsAnalyzer
|
|
49
68
|
case filename
|
50
69
|
when /\.(pl|pm|t|cgi|pod|run)$/i
|
51
70
|
return 'Perl'
|
52
|
-
when /\.rb$/
|
71
|
+
when /\.(?:rb|gemspec)$/
|
72
|
+
return 'Ruby'
|
73
|
+
when /(?:\/|^)Rakefile$/
|
53
74
|
return 'Ruby'
|
54
75
|
when /\.md$/
|
55
76
|
return 'Markdown'
|
@@ -75,9 +96,15 @@ class GitCommitsAnalyzer
|
|
75
96
|
return 'bash'
|
76
97
|
when /(bash|bash_\w+)$/
|
77
98
|
return 'bash'
|
78
|
-
when /\.?(SKIP|gitignore|txt|csv|vim|gitmodules|gitattributes|jshintrc|gperf|vimrc|psqlrc|inputrc|screenrc)$/
|
99
|
+
when /\.?(SKIP|gitignore|txt|csv|vim|gitmodules|gitattributes|jshintrc|gperf|vimrc|psqlrc|inputrc|screenrc|curlrc|wgetrc|selected_editor|dmrc|netrc)$/
|
79
100
|
return 'Text'
|
80
|
-
when
|
101
|
+
when /(?:\/|^)(?:LICENSE|LICENSE-\w+)$/
|
102
|
+
return nil
|
103
|
+
when /\.(?:0|1|VimballRecord)$/
|
104
|
+
return nil
|
105
|
+
when /^vim\/doc\/tags$/
|
106
|
+
return nil
|
107
|
+
when /(?:\/|^)(?:README|MANIFEST|Changes|Gemfile|Gemfile.lock|CHANGELOG)$/
|
81
108
|
return 'Text'
|
82
109
|
end
|
83
110
|
|
@@ -93,6 +120,10 @@ class GitCommitsAnalyzer
|
|
93
120
|
case first_line
|
94
121
|
when /perl$/
|
95
122
|
return 'Perl'
|
123
|
+
when /ruby$/
|
124
|
+
return 'Ruby'
|
125
|
+
when /^\#!\/usr\/bin\/bash$/
|
126
|
+
return 'Ruby'
|
96
127
|
end
|
97
128
|
|
98
129
|
# Fall back on the extension in last resort.
|
@@ -110,23 +141,42 @@ class GitCommitsAnalyzer
|
|
110
141
|
# variables collecting commit metadata.
|
111
142
|
#
|
112
143
|
def parse_repo(repo:)
|
113
|
-
git_repo = Git.open(repo, :
|
144
|
+
git_repo = Git.open(repo, log: @logger)
|
114
145
|
|
146
|
+
#return if repo != '/git_backups/github-guillaumeaubert/dot-files'
|
115
147
|
# Note: override the default of 30 for count(), nil gives the whole git log
|
116
148
|
# history.
|
117
149
|
git_repo.log(count = nil).each do |commit|
|
118
150
|
# Only include the authors specified on the command line.
|
119
151
|
next if !@author.include?(commit.author.email)
|
120
152
|
|
153
|
+
# Parse commit date and update the corresponding stats.
|
154
|
+
commit_datetime = DateTime.parse(commit.author.date.to_s)
|
155
|
+
commit_hour = commit_datetime.hour
|
156
|
+
@commit_hours[commit_hour] += 1
|
157
|
+
commit_day = commit_datetime.strftime('%Y-%m-%d')
|
158
|
+
@commit_days[commit_day] += 1
|
159
|
+
commit_weekday = commit_datetime.strftime('%a')
|
160
|
+
@commit_weekdays_hours[commit_weekday][commit_hour] += 1
|
161
|
+
|
121
162
|
# Parse diff and analyze patches to detect language.
|
122
|
-
diff = commit.
|
163
|
+
diff = git_repo.show(commit.sha)
|
123
164
|
diff.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
|
124
165
|
|
166
|
+
file_properties = git_repo.ls_tree(['-r', commit.sha])
|
167
|
+
|
125
168
|
patches = GitDiffParser.parse(diff)
|
126
169
|
patches.each do |patch|
|
170
|
+
# Skip submodules.
|
171
|
+
next if file_properties['commit'].has_key?(patch.file);
|
172
|
+
|
173
|
+
# Skip symlinks.
|
174
|
+
next if file_properties['blob'].has_key?(patch.file) &&
|
175
|
+
(file_properties['blob'][patch.file][:mode] == '120000')
|
176
|
+
|
127
177
|
body = patch.instance_variable_get :@body
|
128
178
|
language = self.class.determine_language(filename: patch.file, sha: commit.sha, git_repo: git_repo)
|
129
|
-
next if language
|
179
|
+
next if language.nil?
|
130
180
|
@lines_by_language[language] ||=
|
131
181
|
{
|
132
182
|
'added' => 0,
|
@@ -147,10 +197,10 @@ class GitCommitsAnalyzer
|
|
147
197
|
# Add to stats for monthly commit count.
|
148
198
|
# Note: months are zero-padded to allow easy sorting, even if it's more
|
149
199
|
# work for formatting later on.
|
150
|
-
@
|
200
|
+
@commits_by_month[commit.date.strftime("%Y-%m")] += 1
|
151
201
|
|
152
202
|
# Add to stats for total commits count.
|
153
|
-
@
|
203
|
+
@commits_total += 1
|
154
204
|
end
|
155
205
|
end
|
156
206
|
|
@@ -160,8 +210,8 @@ class GitCommitsAnalyzer
|
|
160
210
|
#
|
161
211
|
def get_month_scale()
|
162
212
|
month_scale = []
|
163
|
-
commits_start = @
|
164
|
-
commits_end = @
|
213
|
+
commits_start = @commits_by_month.keys.sort.first.split('-').map { |x| x.to_i }
|
214
|
+
commits_end = @commits_by_month.keys.sort.last.split('-').map { |x| x.to_i }
|
165
215
|
commits_start[0].upto(commits_end[0]) do |year|
|
166
216
|
1.upto(12) do |month|
|
167
217
|
next if month < commits_start[1] && year == commits_start[0]
|
@@ -178,20 +228,23 @@ class GitCommitsAnalyzer
|
|
178
228
|
# Returns: a JSON string.
|
179
229
|
#
|
180
230
|
def to_json()
|
181
|
-
|
231
|
+
formatted_commits_by_month = []
|
182
232
|
month_names = Date::ABBR_MONTHNAMES
|
183
233
|
self.get_month_scale.each do |frame|
|
184
234
|
display_key = month_names[frame[1]] + '-' + frame[0].to_s
|
185
235
|
data_key = sprintf('%s-%02d', frame[0], frame[1])
|
186
|
-
count = @
|
187
|
-
|
236
|
+
count = @commits_by_month[data_key].to_s
|
237
|
+
formatted_commits_by_month << { month: display_key, commits: count.to_s }
|
188
238
|
end
|
189
239
|
|
190
240
|
return JSON.pretty_generate(
|
191
241
|
{
|
192
|
-
:
|
193
|
-
:
|
194
|
-
:
|
242
|
+
commits_total: @commits_total,
|
243
|
+
commits_by_month: formatted_commits_by_month,
|
244
|
+
commits_by_hour: @commit_hours,
|
245
|
+
commits_by_day: @commit_days,
|
246
|
+
commit_by_weekday_hour: @commit_weekdays_hours,
|
247
|
+
lines_by_language: @lines_by_language
|
195
248
|
}
|
196
249
|
)
|
197
250
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-commits-analyzer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Aubert
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Parse git repos and collect commit statistics/data for a given author.
|
14
14
|
email: aubertg@cpan.org
|