blob: fc756928bae72617b2c06f5540d1a042633a91a7 [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
16APPROVED = 'APPROVED'
17PENDING = 'PENDING'
18INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
Edward Lesmesb4f42262020-11-10 23:41:35 +000019
Edward Lesmes91bb7502020-11-06 00:50:24 +000020
Edward Lesmes295dd182020-11-24 23:07:26 +000021def _owner_combinations(owners, num_owners):
22 """Iterate owners combinations by decrasing score.
23
24 The score of an owner is its position on the owners list.
25 The score of a set of owners is the maximum score of all owners on the set.
26
27 Returns all combinations of up to `num_owners` sorted by decreasing score:
28 _owner_combinations(['0', '1', '2', '3'], 2) == [
29 # score 1
30 ('1', '0'),
31 # score 2
32 ('2', '0'),
33 ('2', '1'),
34 # score 3
35 ('3', '0'),
36 ('3', '1'),
37 ('3', '2'),
38 ]
39 """
40 return reversed(list(itertools.combinations(reversed(owners), num_owners)))
41
42
Edward Lesmes91bb7502020-11-06 00:50:24 +000043class OwnersClient(object):
44 """Interact with OWNERS files in a repository.
45
46 This class allows you to interact with OWNERS files in a repository both the
47 Gerrit Code-Owners plugin REST API, and the owners database implemented by
48 Depot Tools in owners.py:
49
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000050 - List all the owners for a group of files.
51 - Check if files have been approved.
52 - Suggest owners for a group of files.
Edward Lesmes91bb7502020-11-06 00:50:24 +000053
54 All code should use this class to interact with OWNERS files instead of the
55 owners database in owners.py
56 """
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000057 def ListOwners(self, path):
Edward Lesmes64e80762020-11-24 19:46:45 +000058 """List all owners for a file.
59
60 The returned list is sorted so that better owners appear first.
61 """
Edward Lesmes91bb7502020-11-06 00:50:24 +000062 raise Exception('Not implemented')
63
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000064 def BatchListOwners(self, paths):
65 """List all owners for a group of files.
66
67 Returns a dictionary {path: [owners]}.
68 """
Gavin Mak99399ca2020-12-11 20:56:03 +000069 with git_common.ScopedPool(kind='threads') as pool:
70 return dict(pool.imap_unordered(
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000071 lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000072
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000073 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000074 """Check the approval status for the given paths.
75
76 Utility method to check for approval status when a change has not yet been
77 created, given reviewers and approvers.
78
79 See GetChangeApprovalStatus for description of the returned value.
80 """
81 approvers = set(approvers)
82 reviewers = set(reviewers)
83 status = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000084 owners_by_path = self.BatchListOwners(paths)
85 for path, owners in owners_by_path.items():
86 owners = set(owners)
87 if owners.intersection(approvers):
Edward Lesmese7d18622020-11-19 23:46:17 +000088 status[path] = APPROVED
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000089 elif owners.intersection(reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000090 status[path] = PENDING
91 else:
92 status[path] = INSUFFICIENT_REVIEWERS
93 return status
94
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000095 def SuggestOwners(self, paths):
Edward Lesmes295dd182020-11-24 23:07:26 +000096 """Suggest a set of owners for the given paths."""
97 paths_by_owner = {}
98 score_by_owner = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000099 owners_by_path = self.BatchListOwners(paths)
100 for path, owners in owners_by_path.items():
Edward Lesmes295dd182020-11-24 23:07:26 +0000101 for i, owner in enumerate(owners):
102 paths_by_owner.setdefault(owner, set()).add(path)
103 # Gerrit API lists owners of a path sorted by an internal score, so
104 # owners that appear first should be prefered.
105 # We define the score of an owner to be their minimum position in all
106 # paths.
107 score_by_owner[owner] = min(i, score_by_owner.get(owner, i))
108
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000109 # Sort owners by their score. Randomize order of owners with same score.
110 owners = sorted(
111 score_by_owner,
112 key=lambda o: (score_by_owner[o], random.random()))
Edward Lesmes295dd182020-11-24 23:07:26 +0000113
114 # Select the minimum number of owners that can approve all paths.
115 # We start at 2 to avoid sending all changes that require multiple reviewers
116 # to top-level owners.
Edward Lesmesca45aff2020-12-03 23:11:01 +0000117 if len(owners) < 2:
118 return owners
119
120 for num_owners in range(2, len(owners)):
Edward Lesmes295dd182020-11-24 23:07:26 +0000121 # Iterate all combinations of `num_owners` by decreasing score, and select
122 # the first one that covers all paths.
123 for selected in _owner_combinations(owners, num_owners):
124 covered = set.union(*(paths_by_owner[o] for o in selected))
125 if len(covered) == len(paths):
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000126 return list(selected)
Edward Lesmes295dd182020-11-24 23:07:26 +0000127
Edward Lesmesb4f42262020-11-10 23:41:35 +0000128
129class DepotToolsClient(OwnersClient):
130 """Implement OwnersClient using owners.py Database."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000131 def __init__(self, root, branch, fopen=open, os_path=os.path):
132 super(DepotToolsClient, self).__init__()
133
Edward Lesmesb4f42262020-11-10 23:41:35 +0000134 self._root = root
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000135 self._branch = branch
Edward Lesmesb4721682020-11-19 22:59:57 +0000136 self._fopen = fopen
137 self._os_path = os_path
Edward Lesmes82b992a2021-01-11 23:24:55 +0000138 self._db = None
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000139 self._db_lock = threading.Lock()
Edward Lesmes829ce022020-11-18 18:30:31 +0000140
Edward Lesmes82b992a2021-01-11 23:24:55 +0000141 def _ensure_db(self):
142 if self._db is not None:
143 return
144 self._db = owners_db.Database(self._root, self._fopen, self._os_path)
145 self._db.override_files = self._GetOriginalOwnersFiles()
146
Edward Lesmes829ce022020-11-18 18:30:31 +0000147 def _GetOriginalOwnersFiles(self):
148 return {
Edward Lesmes8a791e72020-12-02 18:33:18 +0000149 f: scm.GIT.GetOldContents(self._root, f, self._branch).splitlines()
Edward Lesmesd4e6fb62020-11-17 00:17:58 +0000150 for _, f in scm.GIT.CaptureStatus(self._root, self._branch)
151 if os.path.basename(f) == 'OWNERS'
Edward Lesmes829ce022020-11-18 18:30:31 +0000152 }
Edward Lesmesb4f42262020-11-10 23:41:35 +0000153
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000154 def ListOwners(self, path):
155 # all_possible_owners is not thread safe.
156 with self._db_lock:
Edward Lesmes82b992a2021-01-11 23:24:55 +0000157 self._ensure_db()
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000158 # all_possible_owners returns a dict {owner: [(path, distance)]}. We want
159 # to return a list of owners sorted by increasing distance.
160 distance_by_owner = self._db.all_possible_owners([path], None)
161 # We add a small random number to the distance, so that owners at the same
162 # distance are returned in random order to avoid overloading those who
163 # would appear first.
164 return sorted(
165 distance_by_owner,
166 key=lambda o: distance_by_owner[o][0][1] + random.random())
Gavin Makc94b21d2020-12-10 20:27:32 +0000167
168
169class GerritClient(OwnersClient):
170 """Implement OwnersClient using OWNERS REST API."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000171 def __init__(self, host, project, branch):
172 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000173
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000174 self._host = host
175 self._project = project
176 self._branch = branch
177
178 def ListOwners(self, path):
Gavin Makc94b21d2020-12-10 20:27:32 +0000179 # GetOwnersForFile returns a list of account details sorted by order of
180 # best reviewer for path. If code owners have the same score, the order is
181 # random.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000182 data = gerrit_util.GetOwnersForFile(
183 self._host, self._project, self._branch, path)
Gavin Makc94b21d2020-12-10 20:27:32 +0000184 return [d['account']['email'] for d in data]