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 +4 -4
- data/Gemfile.lock +1 -1
- data/bin/git_ownership_insights +99 -12
- data/lib/git_ownership_insights/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0317f67423ade18e56b854a07912a388bece13db418ab72dd8a678ee18c802a5
|
4
|
+
data.tar.gz: f845947afad20fde877e4ed2568934a8bee8299d14d877a352386b5a899928d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3a3d4aedbf40a3928dadd38ab0442382828fe0a3a0712fbf6e15978105ccad2fcbac8102318bd6b70363e779036ff7da7de06e8d55b525858a4b0bf88dbb266
|
7
|
+
data.tar.gz: b67f652e9e3f9676fa6455dc452a04700a48127aef953cb0d99ced1a722e3ad35dbed035fe37435561a349e72cd36e2e5881947f4b5128f2a29a334324144284
|
data/Gemfile.lock
CHANGED
data/bin/git_ownership_insights
CHANGED
@@ -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 =
|
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',
|
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
|
51
|
+
puts <<~EXAMPLES
|
38
52
|
|
39
|
-
Examples:
|
40
|
-
|
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] ||
|
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
|
59
|
-
|
60
|
-
|
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
|
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 "
|
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])
|
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.
|
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-
|
11
|
+
date: 2024-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: date
|