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