git_ownership_insights 1.0.6 → 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b7a7be3eb274378081cac3ffa1c7698dcc17dc1ea5e8dd61b3bfbdedfdee55c
4
- data.tar.gz: 68011fb06d78d3eee4d47e3dcf3bf68e79c759d03acbfee3edaa525d8627d8bc
3
+ metadata.gz: fe758dc63a0e4961606d39b2c484581613564a1ef1fd3644add238a1cd934f6c
4
+ data.tar.gz: a8a442b6c563f81df7342d3a796e2244cbdb8a66215af52ed6be34a4c629b388
5
5
  SHA512:
6
- metadata.gz: 2782093b040f0947b82f0ad29633628afda6ac83824e1532ba8157f569efc7199f11c29b8055ec11073b1436efcb890a5ccff44450aadf1ae17c5c3fcb48d1cc
7
- data.tar.gz: 1e7017a0f1cb0bfe93ff9c6a1193de0fd78f59bf4d17fd0834c4a2390af8dd7fab16680aafd0bc5ffb04b175454c395a2cbea2d79f30f758f04e105335d0aabe
6
+ metadata.gz: 8fbcfe9390bcabac1dc9b716958baf9df93af32640ce309e31f007a6c0dcab69824060c2c6f682e0f52372da0560a9fa78af0b3844b13fc200e28d660a17f79e
7
+ data.tar.gz: 13da08836e586de38dd186bf7054408b10253ddf1cd84a04a4ab305c723efba64b0cf91dfd7e090e16a6347abb7e27bc562ae0b1787e74b917dc4e04f76e87b5
@@ -5,6 +5,7 @@ require 'date'
5
5
  require 'optparse'
6
6
  require 'pry'
7
7
  require_relative '../lib/git_ownership_insights/version'
8
+ require_relative '../lib/git_ownership_insights/git_ownership_insight'
8
9
 
9
10
  options = {}
10
11
  OptionParser.new do |opts|
@@ -26,11 +27,13 @@ OptionParser.new do |opts|
26
27
  options[:hotspot_files] = true
27
28
  end
28
29
 
29
- opts.on('--excluded-contributors STRING', 'Comma-delimited list of excluded contributors [example: WEB,RAILS,MOBILE]') do |exclusions|
30
+ opts.on('--excluded-contributors STRING',
31
+ 'Comma-delimited list of excluded contributors [example: WEB,RAILS,MOBILE]') do |exclusions|
30
32
  options[:exclusions] = exclusions
31
33
  end
32
34
 
33
- opts.on('--excluded-files STRING', 'Comma-delimited list of excluded files [example: ViewController,AppDelegate.swift]') do |excluded_files|
35
+ opts.on('--excluded-files STRING',
36
+ 'Comma-delimited list of excluded files [example: ViewController,AppDelegate.swift]') do |excluded_files|
34
37
  options[:excluded_files] = excluded_files
35
38
  end
36
39
 
@@ -51,11 +54,13 @@ OptionParser.new do |opts|
51
54
  options[:team_regex] = team_regex
52
55
  end
53
56
 
54
- opts.on('--top-contributing-team STRING', 'Limit of top contributed to the directory teams in codeownership data [default: 5]') do |top_contributing_team|
57
+ opts.on('--top-contributing-team STRING',
58
+ 'Limit of top contributed to the directory teams in codeownership data [default: 5]') do |top_contributing_team|
55
59
  options[:top_contributing_team] = top_contributing_team
56
60
  end
57
61
 
58
- opts.on('--top-touched-files STRING', 'Limit of top touched files by individual contributors in codeownership data [default: 5]') do |top_touched_files|
62
+ opts.on('--top-touched-files STRING',
63
+ 'Limit of top touched files by individual contributors in codeownership data [default: 5]') do |top_touched_files|
59
64
  options[:top_touched_files] = top_touched_files
60
65
  end
61
66
 
@@ -63,15 +68,18 @@ OptionParser.new do |opts|
63
68
  options[:codeowners_path] = codeowners_path
64
69
  end
65
70
 
66
- opts.on('--big-file-size STRING', 'The amount of lines in the file to be considered big [default: 250]') do |big_file_size|
71
+ opts.on('--big-file-size STRING',
72
+ 'The amount of lines in the file to be considered big [default: 250]') do |big_file_size|
67
73
  options[:big_file_size] = big_file_size
68
74
  end
69
75
 
70
- opts.on('--default-branch STRING', 'The default branch to pull and run metrics for [default: master]') do |default_branch|
76
+ opts.on('--default-branch STRING',
77
+ 'The default branch to pull and run metrics for [default: master]') do |default_branch|
71
78
  options[:default_branch] = default_branch
72
79
  end
73
80
 
74
- opts.on('--code-extensions STRING', 'The file extensions that consider to be code [default: ".kt, .swift"]') do |code_extension|
81
+ opts.on('--code-extensions STRING',
82
+ 'The file extensions that consider to be code [default: ".kt, .swift"]') do |code_extension|
75
83
  options[:code_extension] = code_extension
76
84
  end
77
85
 
@@ -96,7 +104,7 @@ REPO_PATH = options[:path] || '.'
96
104
  TEAM_REGEX = options[:team_regex] || '[A-Za-z]+'
97
105
  TOP_TOUCHED_FILES = options[:top_touched_files] || 5
98
106
  TOP_CONTRIBUTED_TEAMS = options[:top_contributing_team] || 5
99
- CODEOWNERS_PATH = options[:codeowners_path] || ".github/CODEOWNERS"
107
+ CODEOWNERS_PATH = options[:codeowners_path] || '.github/CODEOWNERS'
100
108
  BIG_FILE_SIZE = options[:big_file_size] || 250
101
109
  CI = options[:ci] || false
102
110
  DEFAULT_BRANCH = options[:default_branch] || 'master'
@@ -105,231 +113,6 @@ HOTSPOT = options[:hotspot_files] || false
105
113
  CODE_EXTENSIONS = options[:code_extension] ? options[:code_extension].split : ['.swift', '.kt']
106
114
  EXCLUDED_FILES = options[:excluded_files]
107
115
 
108
- def true?(obj)
109
- obj.to_s.downcase == "true"
110
- end
111
-
112
- def read_codeowners_file
113
- raise "CODEOWNERS file does not exist under #{CODEOWNERS_PATH}" unless File.exist?(CODEOWNERS_PATH)
114
-
115
- codeowners = {}
116
- File.readlines(CODEOWNERS_PATH).each do |line|
117
- next if line.strip.empty? || line.start_with?('#') # Skip comments and empty lines
118
-
119
- parts = line.split(/\s+/)
120
- directory_pattern = parts[0]
121
- owner = parts[1..].map { |o| o.start_with?('@') ? o[1..] : o }.join(' ') # Remove leading '@' from team names
122
- codeowners[directory_pattern] = owner
123
- end
124
- codeowners
125
- end
126
-
127
- def find_owners(file_path, codeowners)
128
- matching_patterns = codeowners.keys.select do |pattern|
129
- pattern_regex = Regexp.new("^#{Regexp.escape(pattern.sub(%r{^/+}, '').chomp('/')).gsub('\*', '.*').gsub('**', '.*?')}")
130
- file_path =~ pattern_regex
131
- end
132
-
133
- return ['unknown'] if matching_patterns.empty?
134
-
135
- # Sort patterns by length in descending order
136
- sorted_patterns = matching_patterns.sort_by(&:length).reverse
137
-
138
- # Find the most specific matching pattern
139
- best_match = sorted_patterns.find do |pattern|
140
- pattern_regex = Regexp.new("^#{Regexp.escape(pattern.sub(%r{^/+}, '').chomp('/')).gsub('\*', '.*').gsub('**', '.*?')}")
141
- file_path =~ pattern_regex
142
- end
143
-
144
- codeowners[best_match].split(' ')
145
- end
146
-
147
- def count_big_files(directory_path, size: BIG_FILE_SIZE)
148
- size = size.to_i
149
- # Get a list of all files in the specified directory
150
- files = Dir.glob(File.join(directory_path, '**', '*')).select { |file| File.file?(file) }
151
-
152
- code_files = files.select {|f|
153
- extension = File.extname(f)
154
- valid_extensions = ['.swift', '.kt']
155
- valid_extensions.include?(extension)
156
- }
157
-
158
- # Initialize a counter for files that meet the criteria
159
- count = 0
160
-
161
- # Iterate through each file and check the line count
162
- code_files.each do |file|
163
- lines_count = File.foreach(file).reject { |line| line.match(/^\s*(\/\/|\/\*.*\*\/|\s*$)/) }.count
164
-
165
- if lines_count > size
166
- count += 1
167
- end
168
- end
169
-
170
- puts " Total number of code files longer than #{size} lines: #{count}"
171
- end
172
-
173
- def count_hotspot_lines(files)
174
- code_files = files.select {|f|
175
- extension = File.extname(f)
176
- valid_extensions = ['.swift', '.kt']
177
- valid_extensions.include?(extension)
178
- }
179
-
180
- # Initialize a counter for files that meet the criteria
181
- count = 0
182
-
183
- # Iterate through each file and check the line count
184
- code_files.each do |file|
185
- lines_count = File.foreach(file).reject { |line| line.match(/^\s*(\/\/|\/\*.*\*\/|\s*$)/) }.count
186
-
187
- count += lines_count
188
- end
189
-
190
- puts " Total lines of hotspot code: #{count}"
191
- end
192
-
193
- def contribution_message(directory_path:, duration_in_days:, begin_time:, debug: nil, steps: nil)
194
- duration_in_days = duration_in_days.to_i
195
- all_teams = []
196
- teams_count = 0
197
- files_changed_by_many_teams = 0
198
- total_changes = 0
199
- start_date = begin_time.to_time.to_i - duration_in_days * 86_400
200
- end_date = begin_time.to_time.to_i
201
- file_count = `git ls-tree -r --name-only $(git rev-list -1 --since="#{start_date}" --until="#{end_date}" HEAD) -- "#{directory_path}" | wc -l`.to_i
202
- all_files_with_changes = `git log --name-only --pretty=format:"" --since="#{start_date}" --until="#{end_date}" "#{directory_path}"`.split.sort
203
- excluded_patterns = EXCLUDED_FILES.split(',') if EXCLUDED_FILES
204
-
205
- code_files_with_changes = all_files_with_changes.select {|f|
206
- extension = File.extname(f)
207
- valid_extensions = CODE_EXTENSIONS
208
- valid_extensions.include?(extension)
209
- }
210
-
211
- if EXCLUDED_FILES
212
- code_files_with_changes = code_files_with_changes.reject do |file|
213
- excluded_patterns.any? { |pattern| file.include?(pattern) }
214
- end
215
- end
216
-
217
- uniq_code_files_with_changes = code_files_with_changes.uniq
218
-
219
- file_team_map = {}
220
- uniq_code_files_with_changes.each do |file|
221
- filename = File.basename(file)
222
- commit_count = `git log --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}" | grep -c '^commit'`.to_i
223
-
224
- # Get the log of the file in the given duration
225
- git_log = `git log --pretty=format:"%s" --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}"`.split("\n")
226
- teams = git_log.map do |team|
227
- team.match(/#{TEAM_REGEX}/)[0].upcase
228
- end.reject { |e| EXCLUSIONS&.include?(e) }
229
-
230
- total_changes += commit_count
231
- all_teams << teams
232
- teams = teams.uniq
233
-
234
- if teams.count > 1
235
- files_changed_by_many_teams += 1
236
- file_team_map.merge!("#{file}" => [teams, commit_count])
237
- teams_count += teams.count
238
- end
239
-
240
- puts "\n#{filename} [#{commit_count}]:#{teams}\n" if debug
241
- end
242
-
243
- occurrences = all_teams.flatten.compact.tally
244
- sorted_occurrences = occurrences.sort_by { |element, count| [-count, element] }
245
- contributors = Hash[sorted_occurrences]
246
-
247
- churn_count = file_team_map.values.map { |value| value[1] }.sum
248
- hotspot_changes_percentage = (churn_count.to_f / total_changes.to_f)*100
249
-
250
- # Filter files based on extension and size
251
- filtered_files = file_team_map.select do |file_path|
252
- next unless File.exist?(file_path)
253
-
254
- # Check if the file size is more than BIG_FILE_SIZE lines (excluding empty and commented lines)
255
- File.foreach(file_path).reject { |line| line.match(/^\s*(\/\/|\/\*.*\*\/|\s*$)/) }.count > BIG_FILE_SIZE.to_i
256
- end
257
-
258
- filtered_top_touched_files = filtered_files.sort_by { |element, count| [-count.last, element] }
259
-
260
- puts "Timeframe: #{(begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{begin_time.strftime('%Y-%m-%d')}"
261
- puts " Hotspot Code Changes: #{churn_count} (#{hotspot_changes_percentage.round(2)}%)"
262
- puts " Cross-Squad File Dependencies Count: #{teams_count}"
263
- puts " Files exceeding #{BIG_FILE_SIZE} lines with multiple contributors: #{filtered_top_touched_files.count}"
264
- count_hotspot_lines(filtered_files.keys)
265
- count_big_files(directory_path)
266
- puts " Code files with a single contributor: #{(100 - ((files_changed_by_many_teams.to_f / code_files_with_changes.count.to_f) * 100)).round(2)}%"
267
- puts " Total amount of code changes: #{total_changes}"
268
- puts " Total files changed: #{code_files_with_changes.count}"
269
- puts " Total files in the folder: #{file_count}"
270
- puts " Contributors: #{contributors}"
271
-
272
- if HOTSPOT
273
- puts "\n"
274
- puts " Hotspot changes:"
275
- filtered_top_touched_files.each do |line|
276
- puts " #{line.first.gsub(directory_path, '')} Contributors: #{line.last.first} Commits: #{line.last.last}"
277
- end
278
- end
279
-
280
-
281
- if CODEOWNERS
282
- puts "\n"
283
- puts "Code ownership data:"
284
- codeowners = read_codeowners_file
285
-
286
- owners_data = Hash.new do |hash, key|
287
- hash[key] = { directories: Hash.new do |h, k|
288
- h[k] = { files: [] }
289
- end, churn_count: 0 }
290
- end
291
-
292
- file_team_map.each do |file, count|
293
- owners = find_owners(file, codeowners)
294
- owners.each do |owner|
295
- owners_data[owner][:churn_count] += count.last
296
-
297
- dir_path = File.dirname(file)
298
- owners_data[owner][:directories][dir_path][:files] << { name: File.basename(file), count: count }
299
- end
300
- end
301
-
302
- # Sort owners_data by total count in descending order
303
- sorted_owners_data = owners_data.sort_by { |_, data| -data[:churn_count] }
304
-
305
- # Take the last 5 elements
306
- top_owners_data = sorted_owners_data.last(TOP_CONTRIBUTED_TEAMS.to_i)
307
-
308
- converted_team_map = file_team_map.transform_keys { |key| File.basename(key) }
309
-
310
- puts ' Codeownership data:'
311
- top_owners_data.each do |owner, data|
312
- puts " #{owner.split('/').last}:\n Total Count: #{data[:churn_count]}"
313
- data[:directories].each do |dir, dir_data|
314
- puts " Directory: #{dir}\n Top files:"
315
- dir_data[:files].each do |file_data|
316
- next if converted_team_map[File.basename(file_data[:name])].nil?
317
-
318
- contributors = converted_team_map[file_data[:name]]&.first&.empty? ? [ "Excluded contributor" ] : converted_team_map[file_data[:name]].first
319
- puts " #{File.basename(file_data[:name])} - #{file_data[:count].last} #{contributors}}"
320
- end
321
- end
322
- end
323
- end
324
- steps -= 1
325
-
326
- return unless steps.positive?
327
-
328
- system("git checkout `git rev-list -1 --before='#{(begin_time - duration_in_days).strftime("%B %d %Y")}' HEAD`", [ :out, :err ] => File::NULL)
329
- contribution_message(duration_in_days: duration_in_days, directory_path: directory_path,
330
- begin_time: begin_time - duration_in_days, steps: steps, debug: debug)
331
- end
332
-
333
116
  unless CI
334
117
  puts "\nDirectory: #{REPO_PATH}\n"
335
118
  puts "Time period that data is aggregated by: #{options[:duration_in_days]} days"
@@ -348,8 +131,8 @@ unless CI
348
131
  puts "Debug mode is: #{options[:debug] ? 'on' : 'off'}\n\n"
349
132
  end
350
133
 
351
- system("git checkout #{DEFAULT_BRANCH}", [ :out ] => File::NULL)
352
- system("git pull", [ :out, :err ] => File::NULL)
134
+ system("git checkout #{DEFAULT_BRANCH}", [:out] => File::NULL)
135
+ system('git pull', %i[out err] => File::NULL)
353
136
 
354
- contribution_message(duration_in_days: options[:duration_in_days] || 30, directory_path: REPO_PATH,
355
- begin_time: DateTime.now, steps: options[:steps].to_i, debug: options[:debug])
137
+ GitOwnershipInsights.contribution_message(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])
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitOwnershipInsights
4
+ def self.true?(obj)
5
+ obj.to_s.downcase == 'true'
6
+ end
7
+
8
+ def self.read_codeowners_file
9
+ raise "CODEOWNERS file does not exist under #{CODEOWNERS_PATH}" unless File.exist?(CODEOWNERS_PATH)
10
+
11
+ codeowners = {}
12
+ File.readlines(CODEOWNERS_PATH).each do |line|
13
+ next if line.strip.empty? || line.start_with?('#') # Skip comments and empty lines
14
+
15
+ parts = line.split(/\s+/)
16
+ directory_pattern = parts[0]
17
+ owner = parts[1..].map { |o| o.start_with?('@') ? o[1..] : o }.join(' ') # Remove leading '@' from team names
18
+ codeowners[directory_pattern] = owner
19
+ end
20
+ codeowners
21
+ end
22
+
23
+ def self.find_owners(file_path, codeowners)
24
+ matching_patterns = codeowners.keys.select do |pattern|
25
+ pattern_regex = Regexp.new("^#{Regexp.escape(pattern.sub(%r{^/+}, '').chomp('/')).gsub('\*', '.*').gsub('**',
26
+ '.*?')}")
27
+ file_path =~ pattern_regex
28
+ end
29
+
30
+ return ['unknown'] if matching_patterns.empty?
31
+
32
+ # Sort patterns by length in descending order
33
+ sorted_patterns = matching_patterns.sort_by(&:length).reverse
34
+
35
+ # Find the most specific matching pattern
36
+ best_match = sorted_patterns.find do |pattern|
37
+ pattern_regex = Regexp.new("^#{Regexp.escape(pattern.sub(%r{^/+}, '').chomp('/')).gsub('\*', '.*').gsub('**',
38
+ '.*?')}")
39
+ file_path =~ pattern_regex
40
+ end
41
+
42
+ codeowners[best_match].split(' ')
43
+ end
44
+
45
+ def self.count_big_files(directory_path, size: BIG_FILE_SIZE)
46
+ size = size.to_i
47
+ # Get a list of all files in the specified directory
48
+ files = Dir.glob(File.join(directory_path, '**', '*')).select { |file| File.file?(file) }
49
+
50
+ code_files = files.select do |f|
51
+ extension = File.extname(f)
52
+ valid_extensions = ['.swift', '.kt']
53
+ valid_extensions.include?(extension)
54
+ end
55
+
56
+ # Initialize a counter for files that meet the criteria
57
+ count = 0
58
+ # Iterate through each file and check the line count
59
+ code_files.each do |file|
60
+ lines_count = File.foreach(file).reject { |line| line.match(%r{^\s*(//|/\*.*\*/|\s*$)}) }.count
61
+
62
+ count += 1 if lines_count > size
63
+ end
64
+
65
+ puts " Total number of code files longer than #{size} lines: #{count}"
66
+ end
67
+
68
+ def self.count_hotspot_lines(files)
69
+ code_files = files.select do |f|
70
+ extension = File.extname(f)
71
+ valid_extensions = ['.swift', '.kt']
72
+ valid_extensions.include?(extension)
73
+ end
74
+
75
+ # Initialize a counter for files that meet the criteria
76
+ count = 0
77
+
78
+ # Iterate through each file and check the line count
79
+ code_files.each do |file|
80
+ lines_count = File.foreach(file).reject { |line| line.match(%r{^\s*(//|/\*.*\*/|\s*$)}) }.count
81
+
82
+ count += lines_count
83
+ end
84
+
85
+ puts " Total lines of hotspot code: #{count}"
86
+ end
87
+
88
+ def self.contribution_message(directory_path:, duration_in_days:, begin_time:, debug: nil, steps: nil)
89
+ duration_in_days = duration_in_days.to_i
90
+ all_teams = []
91
+ cross_teams_count = 0
92
+ single_ownership_teams_count = 0
93
+ files_changed_by_many_teams = 0
94
+ total_changes = 0
95
+ start_date = begin_time.to_time.to_i - duration_in_days * 86_400
96
+ end_date = begin_time.to_time.to_i
97
+ file_count = `git ls-tree -r --name-only $(git rev-list -1 --since="#{start_date}" --until="#{end_date}" HEAD) -- "#{directory_path}" | wc -l`.to_i
98
+ all_files_with_changes = `git log --name-only --pretty=format:"" --since="#{start_date}" --until="#{end_date}" "#{directory_path}"`.split.sort
99
+ excluded_patterns = EXCLUDED_FILES.split(',') if EXCLUDED_FILES
100
+
101
+ code_files_with_changes = all_files_with_changes.select do |f|
102
+ extension = File.extname(f)
103
+ valid_extensions = CODE_EXTENSIONS
104
+ valid_extensions.include?(extension)
105
+ end
106
+
107
+ if EXCLUDED_FILES
108
+ code_files_with_changes = code_files_with_changes.reject do |file|
109
+ excluded_patterns.any? { |pattern| file.include?(pattern) }
110
+ end
111
+ end
112
+
113
+ uniq_code_files_with_changes = code_files_with_changes.uniq
114
+
115
+ file_team_map = {}
116
+ uniq_code_files_with_changes.each do |file|
117
+ filename = File.basename(file)
118
+ commit_count = `git log --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}" | grep -c '^commit'`.to_i
119
+ # Get the log of the file in the given duration
120
+ git_log = `git log --pretty=format:"%s" --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}"`.split("\n")
121
+ teams = git_log.map do |team|
122
+ team.match(/#{TEAM_REGEX}/)[0].upcase
123
+ end.reject { |e| EXCLUSIONS&.include?(e) }
124
+
125
+ total_changes += commit_count
126
+ all_teams << teams
127
+ teams = teams.uniq
128
+
129
+ if teams.count > 1
130
+ files_changed_by_many_teams += 1
131
+ file_team_map.merge!(file.to_s => [teams, commit_count])
132
+ cross_teams_count += teams.count if File.exist?(file)
133
+ else
134
+ single_ownership_teams_count += 1 if File.exist?(file)
135
+ end
136
+
137
+ puts "\n#{filename} [#{commit_count}]:#{teams}\n" if debug
138
+ end
139
+
140
+ occurrences = all_teams.flatten.compact.tally
141
+ sorted_occurrences = occurrences.sort_by { |element, count| [-count, element] }
142
+ contributors = Hash[sorted_occurrences]
143
+
144
+ churn_count = file_team_map.values.map { |value| value[1] }.sum
145
+ hotspot_changes_percentage = (churn_count.to_f / total_changes) * 100
146
+
147
+ # Filter files based on extension and size
148
+ filtered_files = file_team_map.select do |file_path|
149
+ next unless File.exist?(file_path)
150
+
151
+ # Check if the file size is more than BIG_FILE_SIZE lines (excluding empty and commented lines)
152
+ File.foreach(file_path).reject { |line| line.match(%r{^\s*(//|/\*.*\*/|\s*$)}) }.count > BIG_FILE_SIZE.to_i
153
+ end
154
+
155
+ filtered_top_touched_files = filtered_files.sort_by { |element, count| [-count.last, element] }
156
+
157
+ puts ""
158
+ puts "Timeframe: #{(begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{begin_time.strftime('%Y-%m-%d')}"
159
+ puts " Hotspot Code Changes: #{churn_count} (#{hotspot_changes_percentage.round(2)}%)"
160
+ puts " Cross-Squad Dependency:"
161
+ puts " Squads depending on each other occurrences: #{cross_teams_count}"
162
+ puts " Squads with single responsibility: #{single_ownership_teams_count}"
163
+ puts " Files exceeding #{BIG_FILE_SIZE} lines with multiple contributors: #{filtered_top_touched_files.count}"
164
+ count_hotspot_lines(filtered_files.keys)
165
+ count_big_files(directory_path)
166
+ puts " Code files with a single contributor: #{(100 - ((files_changed_by_many_teams.to_f / code_files_with_changes.count) * 100)).round(2)}%"
167
+ puts " Total amount of code changes: #{total_changes}"
168
+ puts " Total files changed: #{code_files_with_changes.count}"
169
+ puts " Total files in the folder: #{file_count}"
170
+ puts " Contributors: #{contributors}"
171
+
172
+ if HOTSPOT
173
+ puts "\n"
174
+ puts ' Hotspot changes:'
175
+ filtered_top_touched_files.each do |line|
176
+ puts " #{line.first.gsub(directory_path, '')} Contributors: #{line.last.first} Commits: #{line.last.last}"
177
+ end
178
+ end
179
+
180
+ if CODEOWNERS
181
+ puts "\n"
182
+ puts 'Code ownership data:'
183
+ codeowners = read_codeowners_file
184
+
185
+ owners_data = Hash.new do |hash, key|
186
+ hash[key] = { directories: Hash.new do |h, k|
187
+ h[k] = { files: [] }
188
+ end, churn_count: 0 }
189
+ end
190
+
191
+ file_team_map.each do |file, count|
192
+ owners = find_owners(file, codeowners)
193
+ owners.each do |owner|
194
+ owners_data[owner][:churn_count] += count.last
195
+
196
+ dir_path = File.dirname(file)
197
+ owners_data[owner][:directories][dir_path][:files] << { name: File.basename(file), count: count }
198
+ end
199
+ end
200
+
201
+ # Sort owners_data by total count in descending order
202
+ sorted_owners_data = owners_data.sort_by { |_, data| -data[:churn_count] }
203
+
204
+ # Take the last 5 elements
205
+ top_owners_data = sorted_owners_data.last(TOP_CONTRIBUTED_TEAMS.to_i)
206
+
207
+ converted_team_map = file_team_map.transform_keys { |key| File.basename(key) }
208
+
209
+ puts ' Codeownership data:'
210
+ top_owners_data.each do |owner, data|
211
+ puts " #{owner.split('/').last}:\n Total Count: #{data[:churn_count]}"
212
+ data[:directories].each do |dir, dir_data|
213
+ puts " Directory: #{dir}\n Top files:"
214
+ dir_data[:files].each do |file_data|
215
+ next if converted_team_map[File.basename(file_data[:name])].nil?
216
+
217
+ contributors = converted_team_map[file_data[:name]]&.first&.empty? ? ['Excluded contributor'] : converted_team_map[file_data[:name]].first
218
+ puts " #{File.basename(file_data[:name])} - #{file_data[:count].last} #{contributors}}"
219
+ end
220
+ end
221
+ end
222
+ end
223
+ steps -= 1
224
+
225
+ return unless steps.positive?
226
+
227
+ system("git checkout `git rev-list -1 --before='#{(begin_time - duration_in_days).strftime('%B %d %Y')}' HEAD`",
228
+ %i[out err] => File::NULL)
229
+ contribution_message(duration_in_days: duration_in_days, directory_path: directory_path,
230
+ begin_time: begin_time - duration_in_days, steps: steps, debug: debug)
231
+ end
232
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitOwnershipInsights
4
- VERSION = '1.0.6'
4
+ VERSION = '1.0.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git_ownership_insights
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serghei Moret
@@ -61,6 +61,7 @@ files:
61
61
  - bin/setup
62
62
  - git_ownership_insights.gemspec
63
63
  - lib/git_ownership_insights.rb
64
+ - lib/git_ownership_insights/git_ownership_insight.rb
64
65
  - lib/git_ownership_insights/version.rb
65
66
  - sig/git_ownership_insights.rbs
66
67
  - spec/git_ownership_insights_spec.rb