blob: fed27af7920647f10ab610cdfccbdb5003d160e9 [file] [log] [blame]
Edward Lesmes91bb7502020-11-06 00:50:24 +00001# Copyright (c) 2020 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Edward Lesmes295dd182020-11-24 23:07:26 +00005import itertools
Edward Lesmesd4e6fb62020-11-17 00:17:58 +00006import os
Edward Lesmes64e80762020-11-24 19:46:45 +00007import random
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00008import threading
Edward Lesmesd4e6fb62020-11-17 00:17:58 +00009
Edward Lesmes829ce022020-11-18 18:30:31 +000010import gerrit_util
Gavin Mak99399ca2020-12-11 20:56:03 +000011import git_common
Edward Lesmes64e80762020-11-24 19:46:45 +000012import owners as owners_db
Edward Lesmesd4e6fb62020-11-17 00:17:58 +000013import scm
14
15
Edward Lesmes295dd182020-11-24 23:07:26 +000016def _owner_combinations(owners, num_owners):
17 """Iterate owners combinations by decrasing score.
18
19 The score of an owner is its position on the owners list.
20 The score of a set of owners is the maximum score of all owners on the set.
21
22 Returns all combinations of up to `num_owners` sorted by decreasing score:
23 _owner_combinations(['0', '1', '2', '3'], 2) == [
24 # score 1
25 ('1', '0'),
26 # score 2
27 ('2', '0'),
28 ('2', '1'),
29 # score 3
30 ('3', '0'),
31 ('3', '1'),
32 ('3', '2'),
33 ]
34 """
35 return reversed(list(itertools.combinations(reversed(owners), num_owners)))
36
37
Edward Lesmes91bb7502020-11-06 00:50:24 +000038class OwnersClient(object):
39 """Interact with OWNERS files in a repository.
40
41 This class allows you to interact with OWNERS files in a repository both the
42 Gerrit Code-Owners plugin REST API, and the owners database implemented by
43 Depot Tools in owners.py:
44
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000045 - List all the owners for a group of files.
46 - Check if files have been approved.
47 - Suggest owners for a group of files.
Edward Lesmes91bb7502020-11-06 00:50:24 +000048
49 All code should use this class to interact with OWNERS files instead of the
50 owners database in owners.py
51 """
Edward Lesmes071c3b12021-01-15 19:02:59 +000052 # '*' means that everyone can approve.
53 EVERYONE = '*'
54
55 # Possible status of a file.
56 # - INSUFFICIENT_REVIEWERS: The path needs owners approval, but none of its
57 # owners is currently a reviewer of the change.
58 # - PENDING: An owner of this path has been added as reviewer, but approval
59 # has not been given yet.
60 # - APPROVED: The path has been approved by an owner.
Edward Lesmesc40b2402021-01-12 20:03:11 +000061 APPROVED = 'APPROVED'
62 PENDING = 'PENDING'
63 INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
64
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000065 def ListOwners(self, path):
Edward Lesmes64e80762020-11-24 19:46:45 +000066 """List all owners for a file.
67
68 The returned list is sorted so that better owners appear first.
69 """
Edward Lesmes91bb7502020-11-06 00:50:24 +000070 raise Exception('Not implemented')
71
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000072 def BatchListOwners(self, paths):
73 """List all owners for a group of files.
74
75 Returns a dictionary {path: [owners]}.
76 """
Gavin Mak99399ca2020-12-11 20:56:03 +000077 with git_common.ScopedPool(kind='threads') as pool:
78 return dict(pool.imap_unordered(
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000079 lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000080
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000081 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000082 """Check the approval status for the given paths.
83
84 Utility method to check for approval status when a change has not yet been
85 created, given reviewers and approvers.
86
87 See GetChangeApprovalStatus for description of the returned value.
88 """
89 approvers = set(approvers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000090 if approvers:
91 approvers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000092 reviewers = set(reviewers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000093 if reviewers:
94 reviewers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000095 status = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000096 owners_by_path = self.BatchListOwners(paths)
97 for path, owners in owners_by_path.items():
98 owners = set(owners)
99 if owners.intersection(approvers):
Edward Lesmesc40b2402021-01-12 20:03:11 +0000100 status[path] = self.APPROVED
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000101 elif owners.intersection(reviewers):
Edward Lesmesc40b2402021-01-12 20:03:11 +0000102 status[path] = self.PENDING
Edward Lesmese7d18622020-11-19 23:46:17 +0000103 else:
Edward Lesmesc40b2402021-01-12 20:03:11 +0000104 status[path] = self.INSUFFICIENT_REVIEWERS
Edward Lesmese7d18622020-11-19 23:46:17 +0000105 return status
106
Gavin Makd36dbbd2021-01-25 19:34:58 +0000107 def ScoreOwners(self, paths):
108 """Get sorted list of owners for the given paths."""
109 positions_by_owner = {}
110 owners_by_path = self.BatchListOwners(paths)
111 for owners in owners_by_path.values():
112 for i, owner in enumerate(owners):
113 # Gerrit API lists owners of a path sorted by an internal score, so
114 # owners that appear first should be prefered.
115 # We define the score of an owner based on the pair
116 # (# of files owned, minimum position on all owned files)
117 positions_by_owner.setdefault(owner, []).append(i)
118
119 # Sort owners by their score. Rank owners higher for more files owned and
120 # lower for a larger minimum position across all owned files. Randomize
121 # order for owners with same score to avoid bias.
122 return sorted(
123 positions_by_owner,
124 key=lambda o: (-len(positions_by_owner[o]),
125 min(positions_by_owner[o]) + random.random()))
126
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000127 def SuggestOwners(self, paths):
Edward Lesmes295dd182020-11-24 23:07:26 +0000128 """Suggest a set of owners for the given paths."""
129 paths_by_owner = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000130 owners_by_path = self.BatchListOwners(paths)
131 for path, owners in owners_by_path.items():
Gavin Makd36dbbd2021-01-25 19:34:58 +0000132 for owner in owners:
Edward Lesmes295dd182020-11-24 23:07:26 +0000133 paths_by_owner.setdefault(owner, set()).add(path)
Edward Lesmes295dd182020-11-24 23:07:26 +0000134
135 # Select the minimum number of owners that can approve all paths.
Gavin Makd36dbbd2021-01-25 19:34:58 +0000136 # We start at 2 to avoid sending all changes that require multiple
137 # reviewers to top-level owners.
138 owners = self.ScoreOwners(paths)
Edward Lesmesca45aff2020-12-03 23:11:01 +0000139 if len(owners) < 2:
140 return owners
141
142 for num_owners in range(2, len(owners)):
Gavin Makd36dbbd2021-01-25 19:34:58 +0000143 # Iterate all combinations of `num_owners` by decreasing score, and
144 # select the first one that covers all paths.
Edward Lesmes295dd182020-11-24 23:07:26 +0000145 for selected in _owner_combinations(owners, num_owners):
146 covered = set.union(*(paths_by_owner[o] for o in selected))
147 if len(covered) == len(paths):
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000148 return list(selected)
Edward Lesmes295dd182020-11-24 23:07:26 +0000149
Edward Lesmesb4f42262020-11-10 23:41:35 +0000150
151class DepotToolsClient(OwnersClient):
152 """Implement OwnersClient using owners.py Database."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000153 def __init__(self, root, branch, fopen=open, os_path=os.path):
154 super(DepotToolsClient, self).__init__()
155
Edward Lesmesb4f42262020-11-10 23:41:35 +0000156 self._root = root
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000157 self._branch = branch
Edward Lesmesb4721682020-11-19 22:59:57 +0000158 self._fopen = fopen
159 self._os_path = os_path
Edward Lesmes82b992a2021-01-11 23:24:55 +0000160 self._db = None
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000161 self._db_lock = threading.Lock()
Edward Lesmes829ce022020-11-18 18:30:31 +0000162
Edward Lesmes82b992a2021-01-11 23:24:55 +0000163 def _ensure_db(self):
164 if self._db is not None:
165 return
166 self._db = owners_db.Database(self._root, self._fopen, self._os_path)
167 self._db.override_files = self._GetOriginalOwnersFiles()
168
Edward Lesmes829ce022020-11-18 18:30:31 +0000169 def _GetOriginalOwnersFiles(self):
170 return {
Edward Lesmes8a791e72020-12-02 18:33:18 +0000171 f: scm.GIT.GetOldContents(self._root, f, self._branch).splitlines()
Edward Lesmesd4e6fb62020-11-17 00:17:58 +0000172 for _, f in scm.GIT.CaptureStatus(self._root, self._branch)
173 if os.path.basename(f) == 'OWNERS'
Edward Lesmes829ce022020-11-18 18:30:31 +0000174 }
Edward Lesmesb4f42262020-11-10 23:41:35 +0000175
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000176 def ListOwners(self, path):
177 # all_possible_owners is not thread safe.
178 with self._db_lock:
Edward Lesmes82b992a2021-01-11 23:24:55 +0000179 self._ensure_db()
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000180 # all_possible_owners returns a dict {owner: [(path, distance)]}. We want
181 # to return a list of owners sorted by increasing distance.
182 distance_by_owner = self._db.all_possible_owners([path], None)
Gavin Makd36dbbd2021-01-25 19:34:58 +0000183 # We add a small random number to the distance, so that owners at the
184 # same distance are returned in random order to avoid overloading those
185 # who would appear first.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000186 return sorted(
187 distance_by_owner,
188 key=lambda o: distance_by_owner[o][0][1] + random.random())
Gavin Makc94b21d2020-12-10 20:27:32 +0000189
190
191class GerritClient(OwnersClient):
192 """Implement OwnersClient using OWNERS REST API."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000193 def __init__(self, host, project, branch):
194 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000195
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000196 self._host = host
197 self._project = project
198 self._branch = branch
199
200 def ListOwners(self, path):
Gavin Makc94b21d2020-12-10 20:27:32 +0000201 # GetOwnersForFile returns a list of account details sorted by order of
Edward Lesmes071c3b12021-01-15 19:02:59 +0000202 # best reviewer for path. If owners have the same score, the order is
Gavin Makc94b21d2020-12-10 20:27:32 +0000203 # random.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000204 data = gerrit_util.GetOwnersForFile(
205 self._host, self._project, self._branch, path)
Gavin Makc94b21d2020-12-10 20:27:32 +0000206 return [d['account']['email'] for d in data]