git_ownership_insights 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +2 -0
- data/.rubocop.yml +14 -0
- data/.rubocop_todo.yml +58 -0
- data/Gemfile.lock +1 -1
- data/bin/git_ownership_insights +4 -4
- data/git_ownership_insights.gemspec +1 -1
- data/lib/git_ownership_insights/git_ownership_insight.rb +78 -47
- data/lib/git_ownership_insights/version.rb +2 -2
- data/lib/git_ownership_insights.rb +1 -1
- data/sig/git_ownership_insights.rbs +1 -1
- data/spec/fixtures/file.wrong +0 -0
- data/spec/fixtures/file1.swift +333 -0
- data/spec/fixtures/file2.kt +243 -0
- data/spec/fixtures/file3.swift +330 -0
- data/spec/fixtures/file4.swift +235 -0
- data/spec/fixtures/ignore.kt +0 -0
- data/spec/git_ownership_insights_spec.rb +86 -5
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e323781f21d5b045b9fe23e6790f53739fe6bd99e917a9ad2b88c544a9aa3428
|
4
|
+
data.tar.gz: 0c23bd13ad86a4edc00f38a77e5219d18ebd81af84dc8e76764762611780fb1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7302774074f86ca8f5ebd3288f47150a7cc9adf642b2dfe3bc60f0f0ff087a38f9645789b256e923fc578b5d445293be72dcc143d07906a86c6a678e8742a1c4
|
7
|
+
data.tar.gz: 97f58091878d741eac46b1c0a0eb8263cbe7cc7abdba5f8db2c70bf2d12fe4dc0f089496a8e8b7b87531c9bfa4bdb4577820b75afa50576eec9e30184bbdd723
|
data/.github/CODEOWNERS
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
# The behavior of RuboCop can be controlled via the .rubocop.yml
|
4
|
+
# configuration file. It makes it possible to enable/disable
|
5
|
+
# certain cops (checks) and to alter their behavior if they accept
|
6
|
+
# any parameters. The file can be placed either in your home
|
7
|
+
# directory or in some project directory.
|
8
|
+
#
|
9
|
+
# RuboCop will start looking for the configuration file in the directory
|
10
|
+
# where the inspected file is and continue its way up to the root directory.
|
11
|
+
#
|
12
|
+
# See https://docs.rubocop.org/rubocop/configuration
|
13
|
+
AllCops:
|
14
|
+
TargetRubyVersion: 3.2.1
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2024-01-30 07:43:06 UTC using RuboCop version 1.60.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 4
|
10
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Max: 187
|
13
|
+
|
14
|
+
# Offense count: 1
|
15
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
16
|
+
# AllowedMethods: refine
|
17
|
+
Metrics/BlockLength:
|
18
|
+
Max: 69
|
19
|
+
|
20
|
+
# Offense count: 1
|
21
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
22
|
+
Metrics/CyclomaticComplexity:
|
23
|
+
Max: 31
|
24
|
+
|
25
|
+
# Offense count: 4
|
26
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
27
|
+
Metrics/MethodLength:
|
28
|
+
Max: 115
|
29
|
+
|
30
|
+
# Offense count: 1
|
31
|
+
# Configuration parameters: CountComments, CountAsOne.
|
32
|
+
Metrics/ModuleLength:
|
33
|
+
Max: 192
|
34
|
+
|
35
|
+
# Offense count: 1
|
36
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
37
|
+
Metrics/PerceivedComplexity:
|
38
|
+
Max: 32
|
39
|
+
|
40
|
+
# Offense count: 1
|
41
|
+
# Configuration parameters: AllowedConstants.
|
42
|
+
Style/Documentation:
|
43
|
+
Exclude:
|
44
|
+
- 'spec/**/*'
|
45
|
+
- 'test/**/*'
|
46
|
+
- 'lib/git_ownership_insights/git_ownership_insight.rb'
|
47
|
+
|
48
|
+
# Offense count: 1
|
49
|
+
Style/MultilineBlockChain:
|
50
|
+
Exclude:
|
51
|
+
- 'lib/git_ownership_insights/git_ownership_insight.rb'
|
52
|
+
|
53
|
+
# Offense count: 7
|
54
|
+
# This cop supports safe autocorrection (--autocorrect).
|
55
|
+
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
56
|
+
# URISchemes: http, https
|
57
|
+
Layout/LineLength:
|
58
|
+
Max: 150
|
data/Gemfile.lock
CHANGED
data/bin/git_ownership_insights
CHANGED
@@ -33,7 +33,7 @@ OptionParser.new do |opts|
|
|
33
33
|
end
|
34
34
|
|
35
35
|
opts.on('--excluded-files STRING',
|
36
|
-
'Comma-delimited list of excluded
|
36
|
+
'Comma-delimited list of excluded keywords [example: ViewController,AppDelegate.swift]') do |excluded_files|
|
37
37
|
options[:excluded_files] = excluded_files
|
38
38
|
end
|
39
39
|
|
@@ -121,7 +121,7 @@ unless CI
|
|
121
121
|
puts "Code extensions: #{CODE_EXTENSIONS}"
|
122
122
|
puts "Regex to detect the teams identifiers: #{TEAM_REGEX}"
|
123
123
|
puts "Excluded contributors: #{EXCLUSIONS}\n" if EXCLUSIONS
|
124
|
-
puts "Excluded file
|
124
|
+
puts "Excluded file keywords: #{EXCLUDED_FILES.split(',')}\n" if EXCLUDED_FILES
|
125
125
|
puts "Lines of code limit (big files) for the hotspot calculation: #{BIG_FILE_SIZE}"
|
126
126
|
puts "Hotspot detailed output is: #{options[:hotspot_files] ? 'on' : 'off'}\n"
|
127
127
|
puts "CODEOWNERS output is: #{options[:codeowners] ? 'on' : 'off'}\n"
|
@@ -134,5 +134,5 @@ end
|
|
134
134
|
system("git checkout #{DEFAULT_BRANCH}", [:out] => File::NULL)
|
135
135
|
system('git pull', %i[out err] => File::NULL)
|
136
136
|
|
137
|
-
GitOwnershipInsights.
|
138
|
-
|
137
|
+
GitOwnershipInsights.new(duration_in_days: options[:duration_in_days] || 30, directory_path: REPO_PATH,
|
138
|
+
begin_time: DateTime.now, steps: options[:steps].to_i, debug: options[:debug]).contribution_message
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.summary = 'This gem prints git ownership insights'
|
13
13
|
spec.homepage = 'https://rubygems.org'
|
14
14
|
spec.license = 'MIT'
|
15
|
-
spec.required_ruby_version = '>= 2
|
15
|
+
spec.required_ruby_version = '>= 3.2'
|
16
16
|
|
17
17
|
spec.metadata['homepage_uri'] = spec.homepage
|
18
18
|
spec.metadata['source_code_uri'] = 'https://rubygems.org'
|
@@ -1,11 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'pry'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
class GitOwnershipInsights
|
7
|
+
def initialize(directory_path:, duration_in_days:, begin_time:, debug: nil, steps: 1)
|
8
|
+
@directory_path = directory_path
|
9
|
+
@duration_in_days = duration_in_days
|
10
|
+
@begin_time = begin_time
|
11
|
+
@debug = debug
|
12
|
+
@steps = steps
|
13
|
+
end
|
14
|
+
|
15
|
+
def true?(obj)
|
5
16
|
obj.to_s.downcase == 'true'
|
6
17
|
end
|
7
18
|
|
8
|
-
def
|
19
|
+
def read_codeowners_file
|
9
20
|
raise "CODEOWNERS file does not exist under #{CODEOWNERS_PATH}" unless File.exist?(CODEOWNERS_PATH)
|
10
21
|
|
11
22
|
codeowners = {}
|
@@ -20,7 +31,7 @@ module GitOwnershipInsights
|
|
20
31
|
codeowners
|
21
32
|
end
|
22
33
|
|
23
|
-
def
|
34
|
+
def find_owners(file_path, codeowners)
|
24
35
|
matching_patterns = codeowners.keys.select do |pattern|
|
25
36
|
pattern_regex = Regexp.new("^#{Regexp.escape(pattern.sub(%r{^/+}, '').chomp('/')).gsub('\*', '.*').gsub('**',
|
26
37
|
'.*?')}")
|
@@ -42,7 +53,7 @@ module GitOwnershipInsights
|
|
42
53
|
codeowners[best_match].split(' ')
|
43
54
|
end
|
44
55
|
|
45
|
-
def
|
56
|
+
def count_big_files(directory_path, size: BIG_FILE_SIZE)
|
46
57
|
size = size.to_i
|
47
58
|
# Get a list of all files in the specified directory
|
48
59
|
files = Dir.glob(File.join(directory_path, '**', '*')).select { |file| File.file?(file) }
|
@@ -62,10 +73,10 @@ module GitOwnershipInsights
|
|
62
73
|
count += 1 if lines_count > size
|
63
74
|
end
|
64
75
|
|
65
|
-
puts " *
|
76
|
+
puts " *Current(*) total number of code files longer than #{size} lines:* #{count}"
|
66
77
|
end
|
67
78
|
|
68
|
-
def
|
79
|
+
def count_hotspot_lines(files)
|
69
80
|
code_files = files.select do |f|
|
70
81
|
extension = File.extname(f)
|
71
82
|
valid_extensions = CODE_EXTENSIONS
|
@@ -83,43 +94,57 @@ module GitOwnershipInsights
|
|
83
94
|
puts " *Total lines of hotspot code:* #{count}"
|
84
95
|
end
|
85
96
|
|
86
|
-
def
|
97
|
+
def filter_existing_code_files(files)
|
87
98
|
files.select do |f|
|
99
|
+
next unless File.exist?(f)
|
100
|
+
|
101
|
+
if EXCLUDED_FILES
|
102
|
+
excluded_patterns = EXCLUDED_FILES.split(',')
|
103
|
+
next if excluded_patterns.any? { |pattern| f.include?(pattern) }
|
104
|
+
end
|
105
|
+
|
88
106
|
extension = File.extname(f)
|
89
107
|
valid_extensions = CODE_EXTENSIONS
|
90
108
|
valid_extensions.include?(extension)
|
91
109
|
end
|
92
110
|
end
|
93
111
|
|
94
|
-
def
|
95
|
-
|
112
|
+
def git_files(directory_path:)
|
113
|
+
`git ls-tree -r --name-only $(git rev-list -1 HEAD) -- "#{directory_path}"`
|
114
|
+
end
|
115
|
+
|
116
|
+
def files_with_changes(directory_path:, start_date:, end_date:)
|
117
|
+
`git log --name-only --pretty=format:"" --since="#{start_date}" --until="#{end_date}" "#{directory_path}"`
|
118
|
+
end
|
119
|
+
|
120
|
+
def git_commit_count(file:, start_date:, end_date:)
|
121
|
+
`git log --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}" | grep -c '^commit'`
|
122
|
+
end
|
123
|
+
|
124
|
+
def git_commit_info(file:, start_date:, end_date:)
|
125
|
+
`git log --pretty=format:"%s" --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}"`
|
126
|
+
end
|
127
|
+
|
128
|
+
def contribution_message
|
129
|
+
duration_in_days = @duration_in_days.to_i
|
96
130
|
all_teams = []
|
97
131
|
cross_teams_count = 0
|
98
132
|
single_ownership_teams_count = 0
|
99
133
|
files_changed_by_many_teams = 0
|
100
134
|
total_changes = 0
|
101
|
-
start_date = begin_time.to_time.to_i - duration_in_days * 86_400
|
102
|
-
end_date = begin_time.to_time.to_i
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
code_files_with_changes = filter_code_files(all_files_with_changes)
|
108
|
-
|
109
|
-
if EXCLUDED_FILES
|
110
|
-
code_files_with_changes = code_files_with_changes.reject do |file|
|
111
|
-
excluded_patterns.any? { |pattern| file.include?(pattern) }
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
135
|
+
start_date = @begin_time.to_time.to_i - duration_in_days * 86_400 - 30 * 86_400
|
136
|
+
end_date = @begin_time.to_time.to_i - 30 * 86_400
|
137
|
+
git_ls = git_files(directory_path: @directory_path)
|
138
|
+
file_count = filter_existing_code_files(git_ls.split).count
|
139
|
+
all_files_with_changes = files_with_changes(directory_path: @directory_path, start_date:, end_date:).split.sort
|
140
|
+
code_files_with_changes = filter_existing_code_files(all_files_with_changes)
|
115
141
|
uniq_code_files_with_changes = code_files_with_changes.uniq
|
116
142
|
|
117
143
|
file_team_map = {}
|
118
144
|
uniq_code_files_with_changes.each do |file|
|
119
145
|
filename = File.basename(file)
|
120
|
-
commit_count =
|
121
|
-
|
122
|
-
git_log = `git log --pretty=format:"%s" --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}"`.split("\n")
|
146
|
+
commit_count = git_commit_count(file:, start_date:, end_date:).to_i
|
147
|
+
git_log = git_commit_info(file:, start_date:, end_date:).split("\n")
|
123
148
|
teams = git_log.map do |team|
|
124
149
|
team.match(/#{TEAM_REGEX}/)[0].upcase
|
125
150
|
end.reject { |e| EXCLUSIONS&.include?(e) }
|
@@ -131,12 +156,12 @@ module GitOwnershipInsights
|
|
131
156
|
if teams.count > 1
|
132
157
|
files_changed_by_many_teams += 1
|
133
158
|
file_team_map.merge!(file.to_s => [teams, commit_count])
|
134
|
-
cross_teams_count += teams.count
|
159
|
+
cross_teams_count += teams.count
|
135
160
|
else
|
136
|
-
single_ownership_teams_count += 1
|
161
|
+
single_ownership_teams_count += 1
|
137
162
|
end
|
138
163
|
|
139
|
-
puts "\n#{filename} [#{commit_count}]:#{teams}\n" if debug
|
164
|
+
puts "\n#{filename} [#{commit_count}]:#{teams}\n" if @debug
|
140
165
|
end
|
141
166
|
|
142
167
|
occurrences = all_teams.flatten.compact.tally
|
@@ -156,26 +181,32 @@ module GitOwnershipInsights
|
|
156
181
|
|
157
182
|
filtered_top_touched_files = filtered_files.sort_by { |element, count| [-count.last, element] }
|
158
183
|
|
159
|
-
puts
|
160
|
-
puts "*Timeframe:* #{(begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{begin_time.strftime('%Y-%m-%d')}"
|
161
|
-
puts " *
|
162
|
-
puts "
|
184
|
+
puts ''
|
185
|
+
puts "*Timeframe:* #{(@begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{@begin_time.strftime('%Y-%m-%d')}"
|
186
|
+
puts " *Code files with a single contributor:* #{(100 - ((files_changed_by_many_teams.to_f / file_count) * 100)).round(2)}%"
|
187
|
+
puts " *Existing files changed by many teams:* #{files_changed_by_many_teams}"
|
188
|
+
puts " *Current existing #{CODE_EXTENSIONS} files:* #{file_count}"
|
189
|
+
puts ' *Cross-Squad Dependency:*'
|
163
190
|
puts " *Contributions by multiple squads to the same files:* #{cross_teams_count}"
|
164
191
|
puts " *Contributions by single squads contributing to single files:* #{single_ownership_teams_count}"
|
165
|
-
puts " *
|
192
|
+
puts " *Hotspot Code Changes:* #{hotspot_changes_percentage.round(2)}%"
|
193
|
+
puts " *Churn count(commits to files by multiple teams):* #{churn_count}"
|
194
|
+
puts " *Total amount of commits:* #{total_changes}"
|
166
195
|
count_hotspot_lines(filtered_files.keys)
|
167
|
-
|
168
|
-
puts "
|
169
|
-
puts " *Total amount of
|
170
|
-
puts " *Total files changed:* #{
|
171
|
-
|
196
|
+
puts " *#{CODE_EXTENSIONS} files with multiple contributors:* #{file_team_map.count}"
|
197
|
+
puts " *#{CODE_EXTENSIONS} files exceeding #{BIG_FILE_SIZE} lines with multiple contributors:* #{filtered_top_touched_files.count}"
|
198
|
+
puts " *Total amount of commits to #{CODE_EXTENSIONS} files:* #{total_changes}"
|
199
|
+
puts " *Total #{CODE_EXTENSIONS} files changed:* #{uniq_code_files_with_changes.count}"
|
200
|
+
count_big_files(@directory_path)
|
201
|
+
puts " *Current(*) total of #{CODE_EXTENSIONS} files in the folder:* #{file_count}"
|
172
202
|
puts " *Contributors:* #{contributors}"
|
203
|
+
puts "* means that it the current(instant) repository value, all the other metrics are done over #{duration_in_days} days period"
|
173
204
|
|
174
205
|
if HOTSPOT
|
175
206
|
puts "\n"
|
176
207
|
puts ' Hotspot changes:'
|
177
208
|
filtered_top_touched_files.each do |line|
|
178
|
-
puts " #{line.first.gsub(directory_path, '')} Contributors: #{line.last.first} Commits: #{line.last.last}"
|
209
|
+
puts " #{line.first.gsub(@directory_path, '')} Contributors: #{line.last.first} Commits: #{line.last.last}"
|
179
210
|
end
|
180
211
|
end
|
181
212
|
|
@@ -196,7 +227,7 @@ module GitOwnershipInsights
|
|
196
227
|
owners_data[owner][:churn_count] += count.last
|
197
228
|
|
198
229
|
dir_path = File.dirname(file)
|
199
|
-
owners_data[owner][:directories][dir_path][:files] << { name: File.basename(file), count:
|
230
|
+
owners_data[owner][:directories][dir_path][:files] << { name: File.basename(file), count: }
|
200
231
|
end
|
201
232
|
end
|
202
233
|
|
@@ -222,13 +253,13 @@ module GitOwnershipInsights
|
|
222
253
|
end
|
223
254
|
end
|
224
255
|
end
|
225
|
-
steps -= 1
|
256
|
+
@steps -= 1
|
226
257
|
|
227
|
-
return unless steps.positive?
|
258
|
+
return unless @steps.positive?
|
228
259
|
|
229
|
-
system("git checkout `git rev-list -1 --before='#{(begin_time - duration_in_days).strftime('%B %d %Y')}' HEAD`",
|
260
|
+
system("git checkout `git rev-list -1 --before='#{(@begin_time - duration_in_days).strftime('%B %d %Y')}' HEAD`",
|
230
261
|
%i[out err] => File::NULL)
|
231
|
-
|
232
|
-
|
262
|
+
@begin_time -= duration_in_days
|
263
|
+
contribution_message
|
233
264
|
end
|
234
265
|
end
|
File without changes
|