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