-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathcode_ownership.rb
209 lines (172 loc) · 6.62 KB
/
code_ownership.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# frozen_string_literal: true
# typed: strict
require 'set'
require 'code_teams'
require 'sorbet-runtime'
require 'json'
require 'packs-specification'
require 'code_ownership/mapper'
require 'code_ownership/validator'
require 'code_ownership/private'
require 'code_ownership/cli'
require 'code_ownership/configuration'
if defined?(Packwerk)
require 'code_ownership/private/permit_pack_owner_top_level_key'
end
module CodeOwnership
module_function
extend T::Sig
extend T::Helpers
requires_ancestor { Kernel }
GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
def for_file(file)
@for_file ||= T.let(@for_file, T.nilable(T::Hash[String, T.nilable(CodeTeams::Team)]))
@for_file ||= {}
return nil if file.start_with?('./')
return @for_file[file] if @for_file.key?(file)
Private.load_configuration!
owner = T.let(nil, T.nilable(CodeTeams::Team))
Mapper.all.each do |mapper|
owner = mapper.map_file_to_owner(file)
break if owner # TODO: what if there are multiple owners? Should we respond with an error instead of the first match?
end
@for_file[file] = owner
end
sig { params(team: T.any(CodeTeams::Team, String)).returns(String) }
def for_team(team)
team = T.must(CodeTeams.find(team)) if team.is_a?(String)
ownership_information = T.let([], T::Array[String])
ownership_information << "# Code Ownership Report for `#{team.name}` Team"
Private.glob_cache.raw_cache_contents.each do |mapper_description, glob_to_owning_team_map|
ownership_information << "## #{mapper_description}"
ownership_for_mapper = []
glob_to_owning_team_map.each do |glob, owning_team|
next if owning_team != team
ownership_for_mapper << "- #{glob}"
end
if ownership_for_mapper.empty?
ownership_information << 'This team owns nothing in this category.'
else
ownership_information += ownership_for_mapper.sort
end
ownership_information << ''
end
ownership_information.join("\n")
end
class InvalidCodeOwnershipConfigurationError < StandardError
end
sig { params(filename: String).void }
def self.remove_file_annotation!(filename)
Private::OwnershipMappers::FileAnnotations.new.remove_file_annotation!(filename)
end
sig do
params(
autocorrect: T::Boolean,
stage_changes: T::Boolean,
files: T.nilable(T::Array[String])
).void
end
def validate!(
autocorrect: true,
stage_changes: true,
files: nil
)
Private.load_configuration!
tracked_file_subset = if files
files.select { |f| Private.file_tracked?(f) }
else
Private.tracked_files
end
Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
end
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
# first line that corresponds to a file with assigned ownership
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
def for_backtrace(backtrace, excluded_teams: [])
first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
end
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
# first owned file in it, useful for figuring out which file is being blamed.
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
backtrace_with_ownership(backtrace).each do |(team, file)|
if team && !excluded_teams.include?(team)
return [team, file]
end
end
nil
end
sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[[T.nilable(::CodeTeams::Team), String]]) }
def backtrace_with_ownership(backtrace)
return [] unless backtrace
# The pattern for a backtrace hasn't changed in forever and is considered
# stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
#
# This pattern matches a line like the following:
#
# ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
#
backtrace_line = if RUBY_VERSION >= '3.4.0'
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
'(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
else
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
`(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
end
backtrace.lazy.filter_map do |line|
match = line.match(backtrace_line)
next unless match
file = T.must(match[:file])
[
CodeOwnership.for_file(file),
file
]
end
end
private_class_method(:backtrace_with_ownership)
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
def for_class(klass)
@memoized_values ||= T.let(@memoized_values, T.nilable(T::Hash[String, T.nilable(::CodeTeams::Team)]))
@memoized_values ||= {}
# We use key because the memoized value could be `nil`
if @memoized_values.key?(klass.to_s)
@memoized_values[klass.to_s]
else
path = Private.path_from_klass(klass)
return nil if path.nil?
value_to_memoize = for_file(path)
@memoized_values[klass.to_s] = value_to_memoize
value_to_memoize
end
end
sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
def for_package(package)
Private::OwnershipMappers::PackageOwnership.new.owner_for_package(package)
end
# Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
# Namely, the set of files, packages, and directories which are tracked for ownership should not change.
# The primary reason this is helpful is for clients of CodeOwnership who want to test their code, and each test context
# has different ownership and tracked files.
sig { void }
def self.bust_caches!
@for_file = nil
@memoized_values = nil
Private.bust_caches!
Mapper.all.each(&:bust_caches!)
end
sig { returns(Configuration) }
def self.configuration
Private.configuration
end
end