git_ownership_insights 1.0.6 → 1.0.7

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 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