blob: 87801acf9fffc6ade0a116dcb609355201993f5c [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
Gavin Maka9677a52022-08-08 22:30:27 +00005import itertools
Edward Lesmesd4e6fb62020-11-17 00:17:58 +00006import os
Edward Lesmes64e80762020-11-24 19:46:45 +00007import random
Gavin Maka9677a52022-08-08 22:30:27 +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
Gavin Maka9677a52022-08-08 22:30:27 +000012import owners as owners_db
13import scm
Edward Lesmesd4e6fb62020-11-17 00:17:58 +000014
15
Edward Lesmes91bb7502020-11-06 00:50:24 +000016class OwnersClient(object):
17 """Interact with OWNERS files in a repository.
18
19 This class allows you to interact with OWNERS files in a repository both the
20 Gerrit Code-Owners plugin REST API, and the owners database implemented by
21 Depot Tools in owners.py:
22
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000023 - List all the owners for a group of files.
24 - Check if files have been approved.
25 - Suggest owners for a group of files.
Edward Lesmes91bb7502020-11-06 00:50:24 +000026
27 All code should use this class to interact with OWNERS files instead of the
28 owners database in owners.py
29 """
Edward Lesmes071c3b12021-01-15 19:02:59 +000030 # '*' means that everyone can approve.
31 EVERYONE = '*'
32
33 # Possible status of a file.
34 # - INSUFFICIENT_REVIEWERS: The path needs owners approval, but none of its
35 # owners is currently a reviewer of the change.
36 # - PENDING: An owner of this path has been added as reviewer, but approval
37 # has not been given yet.
38 # - APPROVED: The path has been approved by an owner.
Edward Lesmesc40b2402021-01-12 20:03:11 +000039 APPROVED = 'APPROVED'
40 PENDING = 'PENDING'
41 INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
42
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000043 def ListOwners(self, path):
Edward Lesmes64e80762020-11-24 19:46:45 +000044 """List all owners for a file.
45
46 The returned list is sorted so that better owners appear first.
47 """
Edward Lesmes91bb7502020-11-06 00:50:24 +000048 raise Exception('Not implemented')
49
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000050 def BatchListOwners(self, paths):
51 """List all owners for a group of files.
52
53 Returns a dictionary {path: [owners]}.
54 """
Gavin Mak99399ca2020-12-11 20:56:03 +000055 with git_common.ScopedPool(kind='threads') as pool:
56 return dict(pool.imap_unordered(
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000057 lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000058
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000059 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000060 """Check the approval status for the given paths.
61
62 Utility method to check for approval status when a change has not yet been
63 created, given reviewers and approvers.
64
65 See GetChangeApprovalStatus for description of the returned value.
66 """
67 approvers = set(approvers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000068 if approvers:
69 approvers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000070 reviewers = set(reviewers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000071 if reviewers:
72 reviewers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000073 status = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000074 owners_by_path = self.BatchListOwners(paths)
75 for path, owners in owners_by_path.items():
76 owners = set(owners)
77 if owners.intersection(approvers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000078 status[path] = self.APPROVED
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000079 elif owners.intersection(reviewers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000080 status[path] = self.PENDING
Edward Lesmese7d18622020-11-19 23:46:17 +000081 else:
Edward Lesmesc40b2402021-01-12 20:03:11 +000082 status[path] = self.INSUFFICIENT_REVIEWERS
Edward Lesmese7d18622020-11-19 23:46:17 +000083 return status
84
Edward Lesmes0e2aee72021-02-03 20:12:46 +000085 def ScoreOwners(self, paths, exclude=None):
Gavin Makd36dbbd2021-01-25 19:34:58 +000086 """Get sorted list of owners for the given paths."""
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000087 if not paths:
88 return []
Edward Lesmes0e2aee72021-02-03 20:12:46 +000089 exclude = exclude or []
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000090 owners = []
91 queues = self.BatchListOwners(paths).values()
92 for i in range(max(len(q) for q in queues)):
93 for q in queues:
94 if i < len(q) and q[i] not in owners and q[i] not in exclude:
95 owners.append(q[i])
96 return owners
Gavin Makd36dbbd2021-01-25 19:34:58 +000097
Edward Lesmes0e2aee72021-02-03 20:12:46 +000098 def SuggestOwners(self, paths, exclude=None):
Edward Lesmes295dd182020-11-24 23:07:26 +000099 """Suggest a set of owners for the given paths."""
Edward Lesmes0e2aee72021-02-03 20:12:46 +0000100 exclude = exclude or []
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000101
Edward Lesmes295dd182020-11-24 23:07:26 +0000102 paths_by_owner = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000103 owners_by_path = self.BatchListOwners(paths)
104 for path, owners in owners_by_path.items():
Gavin Makd36dbbd2021-01-25 19:34:58 +0000105 for owner in owners:
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000106 paths_by_owner.setdefault(owner, set()).add(path)
Edward Lesmes295dd182020-11-24 23:07:26 +0000107
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000108 selected = []
109 missing = set(paths)
110 for owner in self.ScoreOwners(paths, exclude=exclude):
111 missing_len = len(missing)
112 missing.difference_update(paths_by_owner[owner])
113 if missing_len > len(missing):
114 selected.append(owner)
115 if not missing:
116 break
Edward Lesmesca45aff2020-12-03 23:11:01 +0000117
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000118 return selected
Edward Lesmes0e2aee72021-02-03 20:12:46 +0000119
Gavin Maka9677a52022-08-08 22:30:27 +0000120
121class DepotToolsClient(OwnersClient):
122 """Implement OwnersClient using owners.py Database."""
123 def __init__(self, root, branch, fopen=open, os_path=os.path):
124 super(DepotToolsClient, self).__init__()
125
126 self._root = root
127 self._branch = branch
128 self._fopen = fopen
129 self._os_path = os_path
130 self._db = None
131 self._db_lock = threading.Lock()
132
133 def _ensure_db(self):
134 if self._db is not None:
135 return
136 self._db = owners_db.Database(self._root, self._fopen, self._os_path)
137 self._db.override_files = self._GetOriginalOwnersFiles()
138
139 def _GetOriginalOwnersFiles(self):
140 return {
141 f: scm.GIT.GetOldContents(self._root, f, self._branch).splitlines()
142 for _, f in scm.GIT.CaptureStatus(self._root, self._branch)
143 if os.path.basename(f) == 'OWNERS'
144 }
145
146 def ListOwners(self, path):
147 # all_possible_owners is not thread safe.
148 with self._db_lock:
149 self._ensure_db()
150 # all_possible_owners returns a dict {owner: [(path, distance)]}. We want
151 # to return a list of owners sorted by increasing distance.
152 distance_by_owner = self._db.all_possible_owners([path], None)
153 # We add a small random number to the distance, so that owners at the
154 # same distance are returned in random order to avoid overloading those
155 # who would appear first.
156 return sorted(
157 distance_by_owner,
158 key=lambda o: distance_by_owner[o][0][1] + random.random())
159
160
Gavin Makc94b21d2020-12-10 20:27:32 +0000161class GerritClient(OwnersClient):
162 """Implement OwnersClient using OWNERS REST API."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000163 def __init__(self, host, project, branch):
164 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000165
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000166 self._host = host
167 self._project = project
168 self._branch = branch
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000169 self._owners_cache = {}
Gavin Make0fee9f2022-08-10 23:41:55 +0000170 self._best_owners_cache = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000171
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000172 # Seed used by Gerrit to shuffle code owners that have the same score. Can
173 # be used to make the sort order stable across several requests, e.g. to get
174 # the same set of random code owners for different file paths that have the
175 # same code owners.
176 self._seed = random.getrandbits(30)
177
Gavin Make0fee9f2022-08-10 23:41:55 +0000178 def _FetchOwners(self, path, cache, highest_score_only=False):
Edward Lesmes5e37f6d2021-02-17 23:32:16 +0000179 # Always use slashes as separators.
180 path = path.replace(os.sep, '/')
Gavin Make0fee9f2022-08-10 23:41:55 +0000181 if path not in cache:
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000182 # GetOwnersForFile returns a list of account details sorted by order of
183 # best reviewer for path. If owners have the same score, the order is
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000184 # random, seeded by `self._seed`.
Gavin Make0fee9f2022-08-10 23:41:55 +0000185 data = gerrit_util.GetOwnersForFile(self._host,
186 self._project,
187 self._branch,
188 path,
189 resolve_all_users=False,
190 highest_score_only=highest_score_only,
191 seed=self._seed)
192 cache[path] = [
193 d['account']['email'] for d in data['code_owners']
194 if 'account' in d and 'email' in d['account']
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000195 ]
Gavin Mak7d690052021-02-25 19:14:22 +0000196 # If owned_by_all_users is true, add everyone as an owner at the end of
197 # the owners list.
198 if data.get('owned_by_all_users', False):
Gavin Make0fee9f2022-08-10 23:41:55 +0000199 cache[path].append(self.EVERYONE)
200 return cache[path]
201
202 def ListOwners(self, path):
203 return self._FetchOwners(path, self._owners_cache)
204
205 def ListBestOwners(self, path):
206 return self._FetchOwners(path,
207 self._best_owners_cache,
208 highest_score_only=True)
209
210 def BatchListBestOwners(self, paths):
211 """List only the higest-scoring owners for a group of files.
212
213 Returns a dictionary {path: [owners]}.
214 """
215 with git_common.ScopedPool(kind='threads') as pool:
216 return dict(
217 pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)), paths))
Edward Lesmes110823b2021-02-05 21:42:27 +0000218
219
Gavin Maka9677a52022-08-08 22:30:27 +0000220def GetCodeOwnersClient(root, upstream, host, project, branch):
Edward Lesmes110823b2021-02-05 21:42:27 +0000221 """Get a new OwnersClient.
222
Gavin Maka9677a52022-08-08 22:30:27 +0000223 Defaults to GerritClient, and falls back to DepotToolsClient if code-owners
224 plugin is not available."""
Edward Lesmes8170c292021-03-19 20:04:43 +0000225 if gerrit_util.IsCodeOwnersEnabledOnHost(host):
Edward Lesmes88f712e2021-03-15 17:55:13 +0000226 return GerritClient(host, project, branch)
Gavin Maka9677a52022-08-08 22:30:27 +0000227 return DepotToolsClient(root, upstream)