blob: d40d799fa9d1b2a3911f51114b97bcb7a8d04c82 [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 Lesmesc40b2402021-01-12 20:03:11 +000052 APPROVED = 'APPROVED'
53 PENDING = 'PENDING'
54 INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
55
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000056 def ListOwners(self, path):
Edward Lesmes64e80762020-11-24 19:46:45 +000057 """List all owners for a file.
58
59 The returned list is sorted so that better owners appear first.
60 """
Edward Lesmes91bb7502020-11-06 00:50:24 +000061 raise Exception('Not implemented')
62
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000063 def BatchListOwners(self, paths):
64 """List all owners for a group of files.
65
66 Returns a dictionary {path: [owners]}.
67 """
Gavin Mak99399ca2020-12-11 20:56:03 +000068 with git_common.ScopedPool(kind='threads') as pool:
69 return dict(pool.imap_unordered(
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000070 lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000071
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000072 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000073 """Check the approval status for the given paths.
74
75 Utility method to check for approval status when a change has not yet been
76 created, given reviewers and approvers.
77
78 See GetChangeApprovalStatus for description of the returned value.
79 """
80 approvers = set(approvers)
81 reviewers = set(reviewers)
82 status = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000083 owners_by_path = self.BatchListOwners(paths)
84 for path, owners in owners_by_path.items():
85 owners = set(owners)
86 if owners.intersection(approvers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000087 status[path] = self.APPROVED
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000088 elif owners.intersection(reviewers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000089 status[path] = self.PENDING
Edward Lesmese7d18622020-11-19 23:46:17 +000090 else:
Edward Lesmesc40b2402021-01-12 20:03:11 +000091 status[path] = self.INSUFFICIENT_REVIEWERS
Edward Lesmese7d18622020-11-19 23:46:17 +000092 return status
93
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000094 def SuggestOwners(self, paths):
Edward Lesmes295dd182020-11-24 23:07:26 +000095 """Suggest a set of owners for the given paths."""
96 paths_by_owner = {}
97 score_by_owner = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000098 owners_by_path = self.BatchListOwners(paths)
99 for path, owners in owners_by_path.items():
Edward Lesmes295dd182020-11-24 23:07:26 +0000100 for i, owner in enumerate(owners):
101 paths_by_owner.setdefault(owner, set()).add(path)
102 # Gerrit API lists owners of a path sorted by an internal score, so
103 # owners that appear first should be prefered.
104 # We define the score of an owner to be their minimum position in all
105 # paths.
106 score_by_owner[owner] = min(i, score_by_owner.get(owner, i))
107
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000108 # Sort owners by their score. Randomize order of owners with same score.
109 owners = sorted(
110 score_by_owner,
111 key=lambda o: (score_by_owner[o], random.random()))
Edward Lesmes295dd182020-11-24 23:07:26 +0000112
113 # Select the minimum number of owners that can approve all paths.
114 # We start at 2 to avoid sending all changes that require multiple reviewers
115 # to top-level owners.
Edward Lesmesca45aff2020-12-03 23:11:01 +0000116 if len(owners) < 2:
117 return owners
118
119 for num_owners in range(2, len(owners)):
Edward Lesmes295dd182020-11-24 23:07:26 +0000120 # Iterate all combinations of `num_owners` by decreasing score, and select
121 # the first one that covers all paths.
122 for selected in _owner_combinations(owners, num_owners):
123 covered = set.union(*(paths_by_owner[o] for o in selected))
124 if len(covered) == len(paths):
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000125 return list(selected)
Edward Lesmes295dd182020-11-24 23:07:26 +0000126
Edward Lesmesb4f42262020-11-10 23:41:35 +0000127
128class DepotToolsClient(OwnersClient):
129 """Implement OwnersClient using owners.py Database."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000130 def __init__(self, root, branch, fopen=open, os_path=os.path):
131 super(DepotToolsClient, self).__init__()
132
Edward Lesmesb4f42262020-11-10 23:41:35 +0000133 self._root = root
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000134 self._branch = branch
Edward Lesmesb4721682020-11-19 22:59:57 +0000135 self._fopen = fopen
136 self._os_path = os_path
Edward Lesmes82b992a2021-01-11 23:24:55 +0000137 self._db = None
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000138 self._db_lock = threading.Lock()
Edward Lesmes829ce022020-11-18 18:30:31 +0000139
Edward Lesmes82b992a2021-01-11 23:24:55 +0000140 def _ensure_db(self):
141 if self._db is not None:
142 return
143 self._db = owners_db.Database(self._root, self._fopen, self._os_path)
144 self._db.override_files = self._GetOriginalOwnersFiles()
145
Edward Lesmes829ce022020-11-18 18:30:31 +0000146 def _GetOriginalOwnersFiles(self):
147 return {
Edward Lesmes8a791e72020-12-02 18:33:18 +0000148 f: scm.GIT.GetOldContents(self._root, f, self._branch).splitlines()
Edward Lesmesd4e6fb62020-11-17 00:17:58 +0000149 for _, f in scm.GIT.CaptureStatus(self._root, self._branch)
150 if os.path.basename(f) == 'OWNERS'
Edward Lesmes829ce022020-11-18 18:30:31 +0000151 }
Edward Lesmesb4f42262020-11-10 23:41:35 +0000152
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000153 def ListOwners(self, path):
154 # all_possible_owners is not thread safe.
155 with self._db_lock:
Edward Lesmes82b992a2021-01-11 23:24:55 +0000156 self._ensure_db()
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000157 # all_possible_owners returns a dict {owner: [(path, distance)]}. We want
158 # to return a list of owners sorted by increasing distance.
159 distance_by_owner = self._db.all_possible_owners([path], None)
160 # We add a small random number to the distance, so that owners at the same
161 # distance are returned in random order to avoid overloading those who
162 # would appear first.
163 return sorted(
164 distance_by_owner,
165 key=lambda o: distance_by_owner[o][0][1] + random.random())
Gavin Makc94b21d2020-12-10 20:27:32 +0000166
167
168class GerritClient(OwnersClient):
169 """Implement OwnersClient using OWNERS REST API."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000170 def __init__(self, host, project, branch):
171 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000172
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000173 self._host = host
174 self._project = project
175 self._branch = branch
176
177 def ListOwners(self, path):
Gavin Makc94b21d2020-12-10 20:27:32 +0000178 # GetOwnersForFile returns a list of account details sorted by order of
179 # best reviewer for path. If code owners have the same score, the order is
180 # random.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000181 data = gerrit_util.GetOwnersForFile(
182 self._host, self._project, self._branch, path)
Gavin Makc94b21d2020-12-10 20:27:32 +0000183 return [d['account']['email'] for d in data]