code_ownership 1.23.0

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