• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the 'License');
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an 'AS IS' BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Find main reviewers for git push commands."""
15
16import math
17import random
18from typing import List, Mapping, Set, Union
19
20# To randomly pick one of multiple reviewers, we put them in a List[str]
21# to work with random.choice efficiently.
22# To pick all of multiple reviewers, we use a Set[str].
23
24# A ProjMapping maps a project path string to
25# (1) a single reviewer email address as a string, or
26# (2) a List of multiple reviewers to be randomly picked, or
27# (3) a Set of multiple reviewers to be all added.
28ProjMapping = Mapping[str, Union[str, List[str], Set[str]]]
29
30# Rust crate owners (reviewers).
31RUST_CRATE_OWNERS: ProjMapping = {
32    'rust/crates/anyhow': 'mmaurer@google.com',
33    # more rust crate owners could be added later
34}
35
36PROJ_REVIEWERS: ProjMapping = {
37    # define non-rust project reviewers here
38}
39
40# Combine all roject reviewers.
41PROJ_REVIEWERS.update(RUST_CRATE_OWNERS)
42
43# Estimated number of rust projects, not the actual number.
44# It is only used to make random distribution "fair" among RUST_REVIEWERS.
45# It should not be too small, to spread nicely to multiple reviewers.
46# It should be larger or equal to len(RUST_CRATES_OWNERS).
47NUM_RUST_PROJECTS = 120
48
49# Reviewers for external/rust/crates projects not found in PROJ_REVIEWER.
50# Each person has a quota, the number of projects to review.
51# Sum of these numbers should be greater or equal to NUM_RUST_PROJECTS
52# to avoid error cases in the creation of RUST_REVIEWER_LIST.
53RUST_REVIEWERS: Mapping[str, int] = {
54    'ivanlozano@google.com': 20,
55    'jeffv@google.com': 20,
56    'jgalenson@google.com': 20,
57    'mmaurer@google.com': 20,
58    'srhines@google.com': 20,
59    'tweek@google.com': 20,
60    # If a Rust reviewer needs to take a vacation, comment out the line,
61    # and distribute the quota to other reviewers.
62}
63
64
65# pylint: disable=invalid-name
66def add_proj_count(projects: Mapping[str, float], reviewer: str, n: float):
67    """Add n to the number of projects owned by the reviewer."""
68    if reviewer in projects:
69        projects[reviewer] += n
70    else:
71        projects[reviewer] = n
72
73
74# Random Rust reviewers are selected from RUST_REVIEWER_LIST,
75# which is created from RUST_REVIEWERS and PROJ_REVIEWERS.
76# A person P in RUST_REVIEWERS will occur in the RUST_REVIEWER_LIST N times,
77# if N = RUST_REVIEWERS[P] - (number of projects owned by P in PROJ_REVIEWERS)
78# is greater than 0. N is rounded up by math.ceil.
79def create_rust_reviewer_list() -> List[str]:
80    """Create a list of duplicated reviewers for weighted random selection."""
81    # Count number of projects owned by each reviewer.
82    rust_reviewers = set(RUST_REVIEWERS.keys())
83    projects = {}  # map from owner to number of owned projects
84    for value in PROJ_REVIEWERS.values():
85        if isinstance(value, str):  # single reviewer for a project
86            add_proj_count(projects, value, 1)
87            continue
88        # multiple reviewers share one project, count only rust_reviewers
89        # pylint: disable=bad-builtin
90        reviewers = set(filter(lambda x: x in rust_reviewers, value))
91        if reviewers:
92            count = 1.0 / len(reviewers)  # shared among all reviewers
93            for name in reviewers:
94                add_proj_count(projects, name, count)
95    result = []
96    for (x, n) in RUST_REVIEWERS.items():
97        if x in projects:  # reduce x's quota by the number of assigned ones
98            n = n - projects[x]
99        if n > 0:
100            result.extend([x] * math.ceil(n))
101    if result:
102        return result
103    # Something was wrong or quotas were too small so that nobody
104    # was selected from the RUST_REVIEWERS. Select everyone!!
105    return list(RUST_REVIEWERS.keys())
106
107
108RUST_REVIEWER_LIST: List[str] = create_rust_reviewer_list()
109
110
111def find_reviewers(proj_path: str) -> str:
112    """Returns an empty string or a reviewer parameter(s) for git push."""
113    index = proj_path.find('/external/')
114    if index >= 0:  # full path
115        proj_path = proj_path[(index + len('/external/')):]
116    elif proj_path.startswith('external/'):  # relative path
117        proj_path = proj_path[len('external/'):]
118    if proj_path in PROJ_REVIEWERS:
119        reviewers = PROJ_REVIEWERS[proj_path]
120        # pylint: disable=isinstance-second-argument-not-valid-type
121        if isinstance(reviewers, List):  # pick any one reviewer
122            return 'r=' + random.choice(reviewers)
123        if isinstance(reviewers, Set):  # add all reviewers in sorted order
124            # pylint: disable=bad-builtin
125            return ','.join(map(lambda x: 'r=' + x, sorted(reviewers)))
126        # reviewers must be a string
127        return 'r=' + reviewers
128    if proj_path.startswith('rust/crates/'):
129        return 'r=' + random.choice(RUST_REVIEWER_LIST)
130    return ''
131