code_ownership 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c664c64c94e083083f3187002fc77ffb7d40cbc7c5c76ae0291b9d5c11cd224f
4
+ data.tar.gz: cf6dcc613f67a6e7723fa73a079dabfa8782ddba886441afd8e5608eaa9249ec
5
+ SHA512:
6
+ metadata.gz: 54373f777f534bd2f600510e1ee94b85151bc5016d076d35ef5ed7d140a1d8b53d1bcea24a1af2badebc5939c52c1e2660e066eb49fcbcc4fb7a330cf227664f
7
+ data.tar.gz: c94fd3d689b09a27b8db89b339e3037ec8e6c1fe76413226b22662ce068599ecd30a6aa80e0f90e389a0d1a17bb58afebd650ed4c8bd57523336bd688517f819
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # CodeOwnership
2
+ This gem helps engineering teams declare ownership of code.
3
+
4
+ Check out `lib/code_ownership.rb` to see the public API.
5
+
6
+ Check out `code_ownership_spec.rb` to see examples of how code ownership is used.
7
+
8
+ ## Usage: Declaring Ownership
9
+ There are three ways to declare code ownership using this gem.
10
+ ### Package-Based Ownership
11
+ Package based ownership integrates [`packwerk`](https://github.com/Shopify/packwerk) and has ownership defined per package. To define that all files within a package are owned by one team, configure your `package.yml` like this:
12
+ ```yml
13
+ enforce_dependency: true
14
+ enforce_privacy: true
15
+ metadata:
16
+ owner: Team
17
+ ```
18
+
19
+ ### Glob-Based Ownership
20
+ In your team's configured YML (see [`bigrails-teams`](https://github.com/bigrails/bigrails-teams)), you can set `owned_globs` to be a glob of files your team owns. For example, in `my_team.yml`:
21
+ ```yml
22
+ name: My Team
23
+ owned_globs:
24
+ - app/services/stuff_belonging_to_my_team/**/**
25
+ - app/controllers/other_stuff_belonging_to_my_team/**/**
26
+ ```
27
+ ### File-Annotation Based Ownership
28
+ File annotations are a last resort if there is no clear home for your code. File annotations go at the top of your file, and look like this:
29
+ ```ruby
30
+ # @team MyTeam
31
+ ```
32
+ ## Usage: Reading CodeOwnership
33
+ ### `for_file`
34
+ `CodeOwnership.for_file`, given a relative path to a file returns a `Teams::Team` if there is a team that owns the file, `nil` otherwise.
35
+
36
+ ```ruby
37
+ CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')
38
+ ```
39
+
40
+ Contributor note: If you are making updates to this method or the methods getting used here, please benchmark the performance of the new implementation against the current for both `for_files` and `for_file` (with 1, 100, 1000 files).
41
+
42
+ See `code_ownership_spec.rb` for examples.
43
+
44
+ ### `for_backtrace`
45
+ `CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `Teams::Team`.
46
+
47
+ ```ruby
48
+ CodeOwnership.for_backtrace(exception.backtrace)
49
+ ```
50
+
51
+ This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.
52
+
53
+ See `code_ownership_spec.rb` for an example.
54
+
55
+ ### `for_class`
56
+
57
+ `CodeOwnership.for_class` can be given a class and will either return `nil`, or a `Teams::Team`.
58
+
59
+ ```ruby
60
+ CodeOwnership.for_class(MyClass.name)
61
+ ```
62
+
63
+ Under the hood, this finds the file where the class is defined and returns the owner of that file.
64
+
65
+ See `code_ownership_spec.rb` for an example.
66
+
67
+ ## Usage: Generating a `CODEOWNERS` file
68
+
69
+ A `CODEOWNERS` file defines who owns specific files or paths in a repository. When you run `bin/codeownership validate`, a `.github/CODEOWNERS` file will automatically be generated and updated.
70
+
71
+ ## Proper Configuration & Validation
72
+ CodeOwnership comes with a validation function to ensure the following things are true:
73
+ 1) Only one mechanism is defining file ownership. That is -- you can't have a file annotation on a file owned via package-based or glob-based ownership. This helps make ownership behavior more clear by avoiding concerns about precedence.
74
+ 2) All teams referenced as an owner for any file or package is a valid team (i.e. it's in the list of `Teams.all`).
75
+ 3) All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
76
+ 3) The `.github/CODEOWNERS` file is up to date. This is automatically corrected and staged unless specified otherwise with `bin/codeownership validate --skip-autocorrect --skip-stage`. You can turn this validation off by setting `skip_codeowners_validation: true` in `code_ownership.yml`.
77
+
78
+ CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
79
+
80
+ Here is an example `config/code_ownership.yml`.
81
+ ```yml
82
+ owned_globs:
83
+ - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
84
+ unowned_globs:
85
+ - db/**/*
86
+ - app/services/some_file1.rb
87
+ - app/services/some_file2.rb
88
+ - frontend/javascripts/**/__generated__/**/*
89
+ ```
90
+ You can call the validation function with the Ruby API
91
+ ```ruby
92
+ CodeOwnership.validate!
93
+ ```
94
+ or the CLI
95
+ ```
96
+ bin/codeownership validate
97
+ ```
98
+
99
+ ## Development
100
+
101
+ Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
data/bin/codeownership ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: strict
3
+
4
+ require 'code_ownership'
5
+ CodeOwnership::Cli.run!(ARGV)
@@ -0,0 +1,60 @@
1
+ # typed: true
2
+
3
+ require 'optparse'
4
+ require 'pathname'
5
+
6
+ module CodeOwnership
7
+ class Cli
8
+ def self.run!(argv)
9
+ # Someday we might support other subcommands. When we do that, we can call
10
+ # argv.shift to get the first argument and check if it's a given subcommand.
11
+ command = argv.shift
12
+ if command == 'validate'
13
+ validate!(argv)
14
+ end
15
+ end
16
+
17
+ def self.validate!(argv)
18
+ options = {}
19
+
20
+ parser = OptionParser.new do |opts|
21
+ opts.banner = 'Usage: bin/codeownership validate [options]'
22
+
23
+ opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
24
+ options[:skip_autocorrect] = true
25
+ end
26
+
27
+ opts.on('-d', '--diff', 'Only run validations with staged files') do
28
+ options[:diff] = true
29
+ end
30
+
31
+ opts.on('-s', '--skip-stage', 'Skips staging the CODEOWNERS file') do
32
+ options[:skip_stage] = true
33
+ end
34
+
35
+ opts.on('--help', 'Shows this prompt') do
36
+ puts opts
37
+ exit
38
+ end
39
+ end
40
+ args = parser.order!(argv) {}
41
+ parser.parse!(args)
42
+
43
+ files = if options[:diff]
44
+ ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
45
+ File.exist?(file)
46
+ end
47
+ else
48
+ Private.tracked_files
49
+ end
50
+
51
+ CodeOwnership.validate!(
52
+ files: files,
53
+ autocorrect: !options[:skip_autocorrect],
54
+ stage_changes: !options[:skip_stage]
55
+ )
56
+ end
57
+
58
+ private_class_method :validate!
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+
3
+ module CodeOwnership
4
+ module Private
5
+ class Configuration < T::Struct
6
+ extend T::Sig
7
+ DEFAULT_JS_PACKAGE_PATHS = T.let(['**/'], T::Array[String])
8
+
9
+ const :owned_globs, T::Array[String]
10
+ const :unowned_globs, T::Array[String]
11
+ const :js_package_paths, T::Array[String]
12
+ const :skip_codeowners_validation, T::Boolean
13
+
14
+ sig { returns(Configuration) }
15
+ def self.fetch
16
+ config_hash = YAML.load_file('config/code_ownership.yml')
17
+
18
+ new(
19
+ owned_globs: config_hash.fetch('owned_globs', []),
20
+ unowned_globs: config_hash.fetch('unowned_globs', []),
21
+ js_package_paths: js_package_paths(config_hash),
22
+ skip_codeowners_validation: config_hash.fetch('skip_codeowners_validation', false)
23
+ )
24
+ end
25
+
26
+ sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[String]) }
27
+ def self.js_package_paths(config_hash)
28
+ specified_package_paths = config_hash['js_package_paths']
29
+ if specified_package_paths.nil?
30
+ DEFAULT_JS_PACKAGE_PATHS.dup
31
+ else
32
+ Array(specified_package_paths)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ module OwnershipMappers
8
+ # Calculate, cache, and return a mapping of file names (relative to the root
9
+ # of the repository) to team name.
10
+ #
11
+ # Example:
12
+ #
13
+ # {
14
+ # 'app/models/company.rb' => Team.find('Setup & Onboarding'),
15
+ # ...
16
+ # }
17
+ class FileAnnotations
18
+ extend T::Sig
19
+ include Interface
20
+
21
+ @@map_files_to_owners = T.let({}, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
22
+
23
+ TEAM_PATTERN = T.let(/\A(?:#|\/\/) @team (?<team>.*)\Z/.freeze, Regexp)
24
+
25
+ sig do
26
+ override.params(file: String).
27
+ returns(T.nilable(::Teams::Team))
28
+ end
29
+ def map_file_to_owner(file)
30
+ file_annotation_based_owner(file)
31
+ end
32
+
33
+ sig do
34
+ override.
35
+ params(files: T::Array[String]).
36
+ returns(T::Hash[String, T.nilable(::Teams::Team)])
37
+ end
38
+ def map_files_to_owners(files)
39
+ return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
40
+
41
+ @@map_files_to_owners = files.each_with_object({}) do |filename_relative_to_root, mapping| # rubocop:disable Style/ClassVars
42
+ owner = file_annotation_based_owner(filename_relative_to_root)
43
+ next unless owner
44
+
45
+ mapping[filename_relative_to_root] = owner
46
+ end
47
+ end
48
+
49
+ sig { params(filename: String).returns(T.nilable(Teams::Team)) }
50
+ def file_annotation_based_owner(filename)
51
+ # If for a directory is named with an ownable extension, we need to skip
52
+ # so File.foreach doesn't blow up below. This was needed because Cypress
53
+ # screenshots are saved to a folder with the test suite filename.
54
+ return if File.directory?(filename)
55
+ return unless File.file?(filename)
56
+
57
+ # The annotation should be on line 1 but as of this comment
58
+ # there's no linter installed to enforce that. We therefore check the
59
+ # first line (the Ruby VM makes a single `read(1)` call for 8KB),
60
+ # and if the annotation isn't in the first two lines we assume it
61
+ # doesn't exist.
62
+
63
+ line_1 = File.foreach(filename).first
64
+
65
+ return if !line_1
66
+
67
+ begin
68
+ team = line_1[TEAM_PATTERN, :team]
69
+ rescue ArgumentError => ex
70
+ if ex.message.include?('invalid byte sequence')
71
+ team = nil
72
+ else
73
+ raise
74
+ end
75
+ end
76
+
77
+ return unless team
78
+
79
+ Private.find_team!(
80
+ team,
81
+ filename
82
+ )
83
+ end
84
+
85
+ sig { params(filename: String).void }
86
+ def remove_file_annotation!(filename)
87
+ if file_annotation_based_owner(filename)
88
+ filepath = Pathname.new(filename)
89
+ lines = filepath.read.split("\n")
90
+ new_lines = lines.select { |line| !line[TEAM_PATTERN] }
91
+ # We explicitly add a final new line since splitting by new line when reading the file lines
92
+ # ignores new lines at the ends of files
93
+ # We also remove leading new lines, since there is after a new line after an annotation
94
+ new_file_contents = "#{new_lines.join("\n")}\n".gsub(/\A\n+/, '')
95
+ filepath.write(new_file_contents)
96
+ end
97
+ end
98
+
99
+ sig do
100
+ override.returns(T::Hash[String, T.nilable(::Teams::Team)])
101
+ end
102
+ def codeowners_lines_to_owners
103
+ @@map_files_to_owners = nil # rubocop:disable Style/ClassVars
104
+ map_files_to_owners(Private.tracked_files)
105
+ end
106
+
107
+ sig { override.returns(String) }
108
+ def description
109
+ 'Annotations at the top of file'
110
+ end
111
+
112
+ sig { override.void }
113
+ def bust_caches!
114
+ @@map_files_to_owners = {} # rubocop:disable Style/ClassVars
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ module OwnershipMappers
8
+ module Interface
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ interface!
13
+
14
+ #
15
+ # This should be fast when run with ONE file
16
+ #
17
+ sig do
18
+ abstract.params(file: String).
19
+ returns(T.nilable(::Teams::Team))
20
+ end
21
+ def map_file_to_owner(file)
22
+ end
23
+
24
+ #
25
+ # This should be fast when run with MANY files
26
+ #
27
+ sig do
28
+ abstract.params(files: T::Array[String]).
29
+ returns(T::Hash[String, T.nilable(::Teams::Team)])
30
+ end
31
+ def map_files_to_owners(files)
32
+ end
33
+
34
+ sig do
35
+ abstract.returns(T::Hash[String, T.nilable(::Teams::Team)])
36
+ end
37
+ def codeowners_lines_to_owners
38
+ end
39
+
40
+ sig { abstract.returns(String) }
41
+ def description
42
+ end
43
+
44
+ sig { abstract.void }
45
+ def bust_caches!
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: true
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ module OwnershipMappers
8
+ class JsPackageOwnership
9
+ extend T::Sig
10
+ include Interface
11
+
12
+ @@package_json_cache = T.let({}, T::Hash[String, T.nilable(ParseJsPackages::Package)]) # rubocop:disable Style/ClassVars
13
+
14
+ sig do
15
+ override.params(file: String).
16
+ returns(T.nilable(::Teams::Team))
17
+ end
18
+ def map_file_to_owner(file)
19
+ package = map_file_to_relevant_package(file)
20
+
21
+ return nil if package.nil?
22
+
23
+ owner_for_package(package)
24
+ end
25
+
26
+ sig do
27
+ override.
28
+ params(files: T::Array[String]).
29
+ returns(T::Hash[String, T.nilable(::Teams::Team)])
30
+ end
31
+ def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
32
+ ParseJsPackages.all.each_with_object({}) do |package, res|
33
+ owner = owner_for_package(package)
34
+ next if owner.nil?
35
+
36
+ glob = package.directory.join('**/**').to_s
37
+ Dir.glob(glob).each do |path|
38
+ res[path] = owner
39
+ end
40
+ end
41
+ end
42
+
43
+ #
44
+ # Package ownership ignores the passed in files when generating code owners lines.
45
+ # This is because Package ownership knows that the fastest way to find code owners for package based ownership
46
+ # is to simply iterate over the packages and grab the owner, rather than iterating over each file just to get what package it is in
47
+ # In theory this means that we may generate code owners lines that cover files that are not in the passed in argument,
48
+ # but in practice this is not of consequence because in reality we never really want to generate code owners for only a
49
+ # subset of files, but rather we want code ownership for all files.
50
+ #
51
+ sig do
52
+ override.returns(T::Hash[String, T.nilable(::Teams::Team)])
53
+ end
54
+ def codeowners_lines_to_owners
55
+ ParseJsPackages.all.each_with_object({}) do |package, res|
56
+ owner = owner_for_package(package)
57
+ next if owner.nil?
58
+
59
+ res[package.directory.join('**/**').to_s] = owner
60
+ end
61
+ end
62
+
63
+ sig { override.returns(String) }
64
+ def description
65
+ 'Owner metadata key in package.json'
66
+ end
67
+
68
+ sig { params(package: ParseJsPackages::Package).returns(T.nilable(Teams::Team)) }
69
+ def owner_for_package(package)
70
+ raw_owner_value = package.metadata['owner']
71
+ return nil if !raw_owner_value
72
+
73
+ Private.find_team!(
74
+ raw_owner_value,
75
+ package.name
76
+ )
77
+ end
78
+
79
+ sig { override.void }
80
+ def bust_caches!
81
+ @@package_json_cache = {} # rubocop:disable Style/ClassVars
82
+ end
83
+
84
+ private
85
+
86
+ # takes a file and finds the relevant `package.json` file by walking up the directory
87
+ # structure. Example, given `packages/a/b/c.rb`, this looks for `packages/a/b/package.json`, `packages/a/package.json`,
88
+ # `packages/package.json`, and `package.json` in that order, stopping at the first file to actually exist.
89
+ # We do additional caching so that we don't have to check for file existence every time
90
+ sig { params(file: String).returns(T.nilable(ParseJsPackages::Package)) }
91
+ def map_file_to_relevant_package(file)
92
+ file_path = Pathname.new(file)
93
+ path_components = file_path.each_filename.to_a.map { |path| Pathname.new(path) }
94
+
95
+ (path_components.length - 1).downto(0).each do |i|
96
+ potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
97
+ potential_package_json_path = potential_relative_path_name.
98
+ join(ParseJsPackages::PACKAGE_JSON_NAME)
99
+
100
+ potential_package_json_string = potential_package_json_path.to_s
101
+
102
+ package = nil
103
+ if @@package_json_cache.key?(potential_package_json_string)
104
+ package = @@package_json_cache[potential_package_json_string]
105
+ elsif potential_package_json_path.exist?
106
+ package = ParseJsPackages::Package.from(potential_package_json_path)
107
+
108
+ @@package_json_cache[potential_package_json_string] = package
109
+ else
110
+ @@package_json_cache[potential_package_json_string] = nil
111
+ end
112
+
113
+ return package unless package.nil?
114
+ end
115
+
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: true
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ module OwnershipMappers
8
+ class PackageOwnership
9
+ extend T::Sig
10
+ include Interface
11
+
12
+ @@package_yml_cache = T.let({}, T::Hash[String, T.nilable(ParsePackwerk::Package)]) # rubocop:disable Style/ClassVars
13
+
14
+ sig do
15
+ override.params(file: String).
16
+ returns(T.nilable(::Teams::Team))
17
+ end
18
+ def map_file_to_owner(file)
19
+ package = map_file_to_relevant_package(file)
20
+
21
+ return nil if package.nil?
22
+
23
+ owner_for_package(package)
24
+ end
25
+
26
+ sig do
27
+ override.
28
+ params(files: T::Array[String]).
29
+ returns(T::Hash[String, T.nilable(::Teams::Team)])
30
+ end
31
+ def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
32
+ ParsePackwerk.all.each_with_object({}) do |package, res|
33
+ owner = owner_for_package(package)
34
+ next if owner.nil?
35
+
36
+ glob = package.directory.join('**/**').to_s
37
+ Dir.glob(glob).each do |path|
38
+ res[path] = owner
39
+ end
40
+ end
41
+ end
42
+
43
+ #
44
+ # Package ownership ignores the passed in files when generating code owners lines.
45
+ # This is because Package ownership knows that the fastest way to find code owners for package based ownership
46
+ # is to simply iterate over the packages and grab the owner, rather than iterating over each file just to get what package it is in
47
+ # In theory this means that we may generate code owners lines that cover files that are not in the passed in argument,
48
+ # but in practice this is not of consequence because in reality we never really want to generate code owners for only a
49
+ # subset of files, but rather we want code ownership for all files.
50
+ #
51
+ sig do
52
+ override.returns(T::Hash[String, T.nilable(::Teams::Team)])
53
+ end
54
+ def codeowners_lines_to_owners
55
+ ParsePackwerk.all.each_with_object({}) do |package, res|
56
+ owner = owner_for_package(package)
57
+ next if owner.nil?
58
+
59
+ res[package.directory.join('**/**').to_s] = owner
60
+ end
61
+ end
62
+
63
+ sig { override.returns(String) }
64
+ def description
65
+ 'Owner metadata key in package.yml'
66
+ end
67
+
68
+ sig { params(package: ParsePackwerk::Package).returns(T.nilable(Teams::Team)) }
69
+ def owner_for_package(package)
70
+ raw_owner_value = package.metadata['owner']
71
+ return nil if !raw_owner_value
72
+
73
+ Private.find_team!(
74
+ raw_owner_value,
75
+ package.yml.to_s
76
+ )
77
+ end
78
+
79
+ sig { override.void }
80
+ def bust_caches!
81
+ @@package_yml_cache = {} # rubocop:disable Style/ClassVars
82
+ end
83
+
84
+ private
85
+
86
+ # takes a file and finds the relevant `package.yml` file by walking up the directory
87
+ # structure. Example, given `packs/a/b/c.rb`, this looks for `packs/a/b/package.yml`, `packs/a/package.yml`,
88
+ # `packs/package.yml`, and `package.yml` in that order, stopping at the first file to actually exist.
89
+ # We do additional caching so that we don't have to check for file existence every time
90
+ sig { params(file: String).returns(T.nilable(ParsePackwerk::Package)) }
91
+ def map_file_to_relevant_package(file)
92
+ file_path = Pathname.new(file)
93
+ path_components = file_path.each_filename.to_a.map { |path| Pathname.new(path) }
94
+
95
+ (path_components.length - 1).downto(0).each do |i|
96
+ potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
97
+ potential_package_yml_path = potential_relative_path_name.
98
+ join(ParsePackwerk::PACKAGE_YML_NAME)
99
+
100
+ potential_package_yml_string = potential_package_yml_path.to_s
101
+
102
+ package = nil
103
+ if @@package_yml_cache.key?(potential_package_yml_string)
104
+ package = @@package_yml_cache[potential_package_yml_string]
105
+ elsif potential_package_yml_path.exist?
106
+ package = ParsePackwerk::Package.from(potential_package_yml_path)
107
+
108
+ @@package_yml_cache[potential_package_yml_string] = package
109
+ else
110
+ @@package_yml_cache[potential_package_yml_string] = nil
111
+ end
112
+
113
+ return package unless package.nil?
114
+ end
115
+
116
+ nil
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: true
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ module OwnershipMappers
8
+ class TeamGlobs
9
+ extend T::Sig
10
+ include Interface
11
+
12
+ @@map_files_to_owners = T.let(@map_files_to_owners, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
13
+ @@map_files_to_owners = {} # rubocop:disable Style/ClassVars
14
+ @@codeowners_lines_to_owners = T.let(@codeowners_lines_to_owners, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
15
+ @@codeowners_lines_to_owners = {} # rubocop:disable Style/ClassVars
16
+
17
+ sig do
18
+ override.
19
+ params(files: T::Array[String]).
20
+ returns(T::Hash[String, T.nilable(::Teams::Team)])
21
+ end
22
+ def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
23
+ return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
24
+
25
+ @@map_files_to_owners = Teams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
26
+ TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
27
+ Dir.glob(glob).each do |filename|
28
+ map[filename] = team
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ sig do
35
+ override.params(file: String).
36
+ returns(T.nilable(::Teams::Team))
37
+ end
38
+ def map_file_to_owner(file)
39
+ map_files_to_owners([file])[file]
40
+ end
41
+
42
+ sig do
43
+ override.returns(T::Hash[String, T.nilable(::Teams::Team)])
44
+ end
45
+ def codeowners_lines_to_owners
46
+ return @@codeowners_lines_to_owners if @@codeowners_lines_to_owners&.keys && @@codeowners_lines_to_owners.keys.count > 0
47
+
48
+ @@codeowners_lines_to_owners = Teams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
49
+ TeamPlugins::Ownership.for(team).owned_globs.each do |owned_glob|
50
+ map[owned_glob] = team
51
+ end
52
+ end
53
+ end
54
+
55
+ sig { override.void }
56
+ def bust_caches!
57
+ @@codeowners_lines_to_owners = {} # rubocop:disable Style/ClassVars
58
+ @@map_files_to_owners = {} # rubocop:disable Style/ClassVars
59
+ end
60
+
61
+ sig { override.returns(String) }
62
+ def description
63
+ 'Team-specific owned globs'
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end