git-commits-analyzer 0.1.0 → 1.0.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/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
|