git_ownership_insights 0.1.2 → 0.1.3

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