git_ownership_insights 0.1.2 → 0.1.3

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: b6bcceedce35270688443f386c76ec576b007cfd91c53cbda26b4b1166d3bcdb
4
- data.tar.gz: c9195552bc37379e3f4cc2277604534d575c2eda7a3f6bf75b78fd0ac906dda0
3
+ metadata.gz: 0317f67423ade18e56b854a07912a388bece13db418ab72dd8a678ee18c802a5
4
+ data.tar.gz: f845947afad20fde877e4ed2568934a8bee8299d14d877a352386b5a899928d8
5
5
  SHA512:
6
- metadata.gz: 0ace817610903ddcf63dafd6d54f365c83129ee24990146259db63f0e6fa959e83e9cdc2b2fb146333ba0f01855cec872656582b8f7a5d964535f652fd64d626
7
- data.tar.gz: 929700dcbd1815ac79d0b7b98caba6f41209709c4c29341188c1784fa0083f3d60e21e5263bbf31f3f8f01c60ba59b3aeeef129831942567c8c77ab926977a1a
6
+ metadata.gz: d3a3d4aedbf40a3928dadd38ab0442382828fe0a3a0712fbf6e15978105ccad2fcbac8102318bd6b70363e779036ff7da7de06e8d55b525858a4b0bf88dbb266
7
+ data.tar.gz: b67f652e9e3f9676fa6455dc452a04700a48127aef953cb0d99ced1a722e3ad35dbed035fe37435561a349e72cd36e2e5881947f4b5128f2a29a334324144284
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- git_ownership_insights (0.1.1)
4
+ git_ownership_insights (0.1.3)
5
5
  date
6
6
  pry
7
7
 
@@ -3,10 +3,11 @@
3
3
 
4
4
  require 'date'
5
5
  require 'optparse'
6
+ require 'pry'
6
7
 
7
8
  options = {}
8
9
  OptionParser.new do |opts|
9
- opts.banner = "Usage: git_ownership_insights [options]"
10
+ opts.banner = 'Usage: git_ownership_insights [options]'
10
11
 
11
12
  opts.on('--debug', 'Enable debug mode') do
12
13
  options[:debug] = true
@@ -20,7 +21,8 @@ OptionParser.new do |opts|
20
21
  options[:steps] = steps
21
22
  end
22
23
 
23
- opts.on('--duration_in_days STRING', 'Number of days to aggregate the changes for [default: 30]') do |duration_in_days|
24
+ opts.on('--duration_in_days STRING',
25
+ 'Number of days to aggregate the changes for [default: 30]') do |duration_in_days|
24
26
  options[:duration_in_days] = duration_in_days
25
27
  end
26
28
 
@@ -32,12 +34,24 @@ OptionParser.new do |opts|
32
34
  options[:team_regex] = team_regex
33
35
  end
34
36
 
37
+ opts.on('--top_contributing_team STRING', 'Limit of top contributed to the directory teams in codeownership data [default: 5]') do |top_contributing_team|
38
+ options[:top_contributing_team] = top_contributing_team
39
+ end
40
+
41
+ opts.on('--top_touched_files STRING', 'Limit of top touched files by individual contributors in codeownership data [default: 5]') do |top_touched_files|
42
+ options[:top_touched_files] = top_touched_files
43
+ end
44
+
45
+ opts.on('--codeowners_path STRING', 'Path to CODEOWNERS file [default: .github/CODEOWNERS]') do |codeowners_path|
46
+ options[:codeowners_path] = codeowners_path
47
+ end
48
+
35
49
  opts.on('-h', '--help', 'Display this help message') do
36
50
  puts opts
37
- puts <<-EXAMPLES
51
+ puts <<~EXAMPLES
38
52
 
39
- Examples:
40
- git_ownership_insights --path src/test --exclusions WEB,RAILS --steps 2 --duration_in_days 90 --debug
53
+ Examples:
54
+ git_ownership_insights --path src/test --exclusions WEB,RAILS --steps 2 --duration_in_days 90 --debug
41
55
  EXAMPLES
42
56
  exit
43
57
  end
@@ -45,7 +59,36 @@ end.parse!
45
59
 
46
60
  EXCLUSIONS = options[:exclusions]&.split(',')
47
61
  REPO_PATH = options[:path] || '.'
48
- TEAM_REGEX = options[:team_regex] || "[A-Za-z]+"
62
+ TEAM_REGEX = options[:team_regex] || '[A-Za-z]+'
63
+ TOP_TOUCHED_FILES = options[:top_touched_files] || 5
64
+ TOP_CONTRIBUTED_TEAMS = options[:top_contributing_team] || 5
65
+ CODEOWNERS_PATH = options[:codeowners_path] || ".github/CODEOWNERS"
66
+ def read_codeowners_file
67
+ raise "CODEOWNERS file does not exist under #{CODEOWNERS_PATH}" unless File.exist?(CODEOWNERS_PATH)
68
+
69
+ codeowners = {}
70
+ File.readlines(CODEOWNERS_PATH).each do |line|
71
+ next if line.strip.empty? || line.start_with?('#') # Skip comments and empty lines
72
+
73
+ parts = line.split(/\s+/)
74
+ directory_pattern = parts[0]
75
+ owner = parts[1..].map { |o| o.start_with?('@') ? o[1..] : o }.join(' ') # Remove leading '@' from team names
76
+ codeowners[directory_pattern] = owner
77
+ end
78
+ codeowners
79
+ end
80
+
81
+ def find_owners(file_path, codeowners)
82
+ matching_patterns = codeowners.keys.select { |pattern| file_path.include?(pattern.sub(%r{^/+}, '').chomp('/')) }
83
+ return ['unknown'] if matching_patterns.empty?
84
+
85
+ # Sort patterns by length in descending order
86
+ sorted_patterns = matching_patterns.sort_by(&:length).reverse
87
+
88
+ # Find the most specific matching pattern
89
+ best_match = sorted_patterns.find { |pattern| file_path.include?(pattern.sub(%r{^/+}, '').chomp('/')) }
90
+ codeowners[best_match].split(' ')
91
+ end
49
92
 
50
93
  def contribution_message(directory_path:, duration_in_days:, begin_time:, debug: nil, steps: nil)
51
94
  duration_in_days = duration_in_days.to_i
@@ -55,9 +98,10 @@ def contribution_message(directory_path:, duration_in_days:, begin_time:, debug:
55
98
  start_date = begin_time.to_time.to_i - duration_in_days * 86_400
56
99
  end_date = begin_time.to_time.to_i
57
100
  file_count = `git ls-tree -r --name-only $(git rev-list -1 --before="#{end_date}" HEAD) -- "#{directory_path}" | wc -l`.to_i
58
- files_with_changes = `git log --name-only --pretty=format:"" --since="#{start_date}" --until="#{end_date}" "#{directory_path}"`.split.sort.uniq
59
-
60
- files_with_changes.each do |file|
101
+ files_with_changes = `git log --name-only --pretty=format:"" --since="#{start_date}" --until="#{end_date}" "#{directory_path}"`.split.sort
102
+ uniq_files_with_changes = files_with_changes.uniq
103
+ file_team_map = {}
104
+ uniq_files_with_changes.each do |file|
61
105
  filename = File.basename(file)
62
106
  commit_count = `git log --since="#{start_date}" --until="#{end_date}" --follow -- "#{file}" | grep -c '^commit'`.to_i
63
107
 
@@ -72,7 +116,7 @@ def contribution_message(directory_path:, duration_in_days:, begin_time:, debug:
72
116
  all_teams << teams
73
117
  teams = teams.uniq
74
118
  files_changed_by_many_teams += 1 if teams.count > 1
75
-
119
+ file_team_map.merge!("#{filename}" => teams)
76
120
  puts "\n#{filename} [#{commit_count}]:#{teams}\n" if debug
77
121
  end
78
122
 
@@ -80,7 +124,44 @@ def contribution_message(directory_path:, duration_in_days:, begin_time:, debug:
80
124
  sorted_occurrences = occurrences.sort_by { |element, count| [-count, element] }
81
125
  contributors = Hash[sorted_occurrences]
82
126
 
83
- puts "Timeframe: #{(begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{begin_time.strftime('%Y-%m-%d')}\n Files with a single contributor: #{(100 - ((files_changed_by_many_teams.to_f / files_with_changes.count) * 100)).round(2)}%\n Amount of commits: #{total_changes}\n Total files changed: #{files_with_changes.count}\n Total files in the folder: #{file_count}\n Main contributors: #{contributors}\n\n"
127
+ puts "Timeframe: #{(begin_time - duration_in_days).strftime('%Y-%m-%d')} to #{begin_time.strftime('%Y-%m-%d')}\n Files with a single contributor: #{(100 - ((files_changed_by_many_teams.to_f / files_with_changes.count) * 100)).round(2)}%\n Amount of commits: #{total_changes}\n Total files changed: #{files_with_changes.count}\n Total files in the folder: #{file_count}\n Contributors: #{contributors}\n"
128
+
129
+ touched_files = files_with_changes.flatten.compact.tally
130
+ top_touched_files = touched_files.sort_by { |element, count| [-count, element] }.take(TOP_TOUCHED_FILES.to_i)
131
+ codeowners = read_codeowners_file
132
+
133
+ owners_data = Hash.new do |hash, key|
134
+ hash[key] = { directories: Hash.new do |h, k|
135
+ h[k] = { files: [] }
136
+ end, total_count: 0 }
137
+ end
138
+
139
+ top_touched_files.each do |file, count|
140
+ owners = find_owners(file, codeowners)
141
+ owners.each do |owner|
142
+ owners_data[owner][:total_count] += count
143
+
144
+ dir_path = File.dirname(file)
145
+ owners_data[owner][:directories][dir_path][:files] << { name: File.basename(file), count: count }
146
+ end
147
+ end
148
+
149
+ # Sort owners_data by total count in descending order
150
+ sorted_owners_data = owners_data.sort_by { |_, data| -data[:total_count] }
151
+
152
+ # Take the last 5 elements
153
+ top_owners_data = sorted_owners_data.last(TOP_CONTRIBUTED_TEAMS.to_i)
154
+
155
+ puts ' Codeownership data:'
156
+ top_owners_data.each do |owner, data|
157
+ puts " #{owner.split('/').last}:\n Total Count: #{data[:total_count]}"
158
+ data[:directories].each do |dir, dir_data|
159
+ puts " Directory: #{dir}\n Top files:"
160
+ dir_data[:files].each do |file_data|
161
+ puts " #{File.basename(file_data[:name])} - #{file_data[:count]} #{file_team_map[file_data[:name]]}"
162
+ end
163
+ end
164
+ end
84
165
 
85
166
  steps -= 1
86
167
 
@@ -91,7 +172,13 @@ def contribution_message(directory_path:, duration_in_days:, begin_time:, debug:
91
172
  end
92
173
 
93
174
  puts "\nDirectory: #{REPO_PATH}\n"
94
- puts "Excluded contributors: #{EXCLUSIONS}\n\n" if EXCLUSIONS
175
+ puts "Time period that data is aggregated by: #{options[:duration_in_days]} days"
176
+ puts "Steps to jump in the past: #{options[:steps]}"
177
+ puts "Limit of the teams shown in codeownership data: #{TOP_CONTRIBUTED_TEAMS}"
178
+ puts "Limit of the files shown in codeownership data: #{TOP_TOUCHED_FILES}"
179
+ puts "Regex to detect the teams identifiers: #{TEAM_REGEX}"
180
+ puts "Excluded contributors: #{EXCLUSIONS}\n" if EXCLUSIONS
181
+ puts "Debug mode is: #{options[:debug] ? 'on' : 'off'}\n\n"
95
182
 
96
183
  contribution_message(duration_in_days: options[:duration_in_days] || 30, directory_path: REPO_PATH,
97
184
  begin_time: DateTime.now, steps: options[:steps].to_i, debug: options[:debug])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitOwnershipInsights
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git_ownership_insights
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serghei Moret
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-16 00:00:00.000000000 Z
11
+ date: 2024-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: date