blob: 90de55499f4ef00c690a9460508b93b8c1cfc861 [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 Lesmesd4e6fb62020-11-17 00:17:58 +00005import os
Edward Lesmes64e80762020-11-24 19:46:45 +00006import random
Edward Lesmesd4e6fb62020-11-17 00:17:58 +00007
Edward Lesmes829ce022020-11-18 18:30:31 +00008import gerrit_util
Gavin Mak99399ca2020-12-11 20:56:03 +00009import git_common
Edward Lesmesd4e6fb62020-11-17 00:17:58 +000010
11
Edward Lesmes91bb7502020-11-06 00:50:24 +000012class OwnersClient(object):
13 """Interact with OWNERS files in a repository.
14
15 This class allows you to interact with OWNERS files in a repository both the
16 Gerrit Code-Owners plugin REST API, and the owners database implemented by
17 Depot Tools in owners.py:
18
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000019 - List all the owners for a group of files.
20 - Check if files have been approved.
21 - Suggest owners for a group of files.
Edward Lesmes91bb7502020-11-06 00:50:24 +000022
23 All code should use this class to interact with OWNERS files instead of the
24 owners database in owners.py
25 """
Edward Lesmes071c3b12021-01-15 19:02:59 +000026 # '*' means that everyone can approve.
27 EVERYONE = '*'
28
29 # Possible status of a file.
30 # - INSUFFICIENT_REVIEWERS: The path needs owners approval, but none of its
31 # owners is currently a reviewer of the change.
32 # - PENDING: An owner of this path has been added as reviewer, but approval
33 # has not been given yet.
34 # - APPROVED: The path has been approved by an owner.
Edward Lesmesc40b2402021-01-12 20:03:11 +000035 APPROVED = 'APPROVED'
36 PENDING = 'PENDING'
37 INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
38
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000039 def ListOwners(self, path):
Edward Lesmes64e80762020-11-24 19:46:45 +000040 """List all owners for a file.
41
42 The returned list is sorted so that better owners appear first.
43 """
Edward Lesmes91bb7502020-11-06 00:50:24 +000044 raise Exception('Not implemented')
45
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000046 def BatchListOwners(self, paths):
47 """List all owners for a group of files.
48
49 Returns a dictionary {path: [owners]}.
50 """
Gavin Mak99399ca2020-12-11 20:56:03 +000051 with git_common.ScopedPool(kind='threads') as pool:
52 return dict(pool.imap_unordered(
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000053 lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000054
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000055 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
Edward Lesmese7d18622020-11-19 23:46:17 +000056 """Check the approval status for the given paths.
57
58 Utility method to check for approval status when a change has not yet been
59 created, given reviewers and approvers.
60
61 See GetChangeApprovalStatus for description of the returned value.
62 """
63 approvers = set(approvers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000064 if approvers:
65 approvers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000066 reviewers = set(reviewers)
Edward Lesmes071c3b12021-01-15 19:02:59 +000067 if reviewers:
68 reviewers.add(self.EVERYONE)
Edward Lesmese7d18622020-11-19 23:46:17 +000069 status = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000070 owners_by_path = self.BatchListOwners(paths)
71 for path, owners in owners_by_path.items():
72 owners = set(owners)
73 if owners.intersection(approvers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000074 status[path] = self.APPROVED
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000075 elif owners.intersection(reviewers):
Edward Lesmesc40b2402021-01-12 20:03:11 +000076 status[path] = self.PENDING
Edward Lesmese7d18622020-11-19 23:46:17 +000077 else:
Edward Lesmesc40b2402021-01-12 20:03:11 +000078 status[path] = self.INSUFFICIENT_REVIEWERS
Edward Lesmese7d18622020-11-19 23:46:17 +000079 return status
80
Edward Lesmes0e2aee72021-02-03 20:12:46 +000081 def ScoreOwners(self, paths, exclude=None):
Gavin Makd36dbbd2021-01-25 19:34:58 +000082 """Get sorted list of owners for the given paths."""
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000083 if not paths:
84 return []
Edward Lesmes0e2aee72021-02-03 20:12:46 +000085 exclude = exclude or []
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000086 owners = []
87 queues = self.BatchListOwners(paths).values()
88 for i in range(max(len(q) for q in queues)):
89 for q in queues:
90 if i < len(q) and q[i] not in owners and q[i] not in exclude:
91 owners.append(q[i])
92 return owners
Gavin Makd36dbbd2021-01-25 19:34:58 +000093
Edward Lesmes0e2aee72021-02-03 20:12:46 +000094 def SuggestOwners(self, paths, exclude=None):
Edward Lesmes295dd182020-11-24 23:07:26 +000095 """Suggest a set of owners for the given paths."""
Edward Lesmes0e2aee72021-02-03 20:12:46 +000096 exclude = exclude or []
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000097
Edward Lesmes295dd182020-11-24 23:07:26 +000098 paths_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():
Gavin Makd36dbbd2021-01-25 19:34:58 +0000101 for owner in owners:
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000102 paths_by_owner.setdefault(owner, set()).add(path)
Edward Lesmes295dd182020-11-24 23:07:26 +0000103
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000104 selected = []
105 missing = set(paths)
106 for owner in self.ScoreOwners(paths, exclude=exclude):
107 missing_len = len(missing)
108 missing.difference_update(paths_by_owner[owner])
109 if missing_len > len(missing):
110 selected.append(owner)
111 if not missing:
112 break
Edward Lesmesca45aff2020-12-03 23:11:01 +0000113
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000114 return selected
Edward Lesmes0e2aee72021-02-03 20:12:46 +0000115
Gavin Makc94b21d2020-12-10 20:27:32 +0000116class GerritClient(OwnersClient):
117 """Implement OwnersClient using OWNERS REST API."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000118 def __init__(self, host, project, branch):
119 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000120
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000121 self._host = host
122 self._project = project
123 self._branch = branch
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000124 self._owners_cache = {}
Gavin Make0fee9f2022-08-10 23:41:55 +0000125 self._best_owners_cache = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000126
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000127 # Seed used by Gerrit to shuffle code owners that have the same score. Can
128 # be used to make the sort order stable across several requests, e.g. to get
129 # the same set of random code owners for different file paths that have the
130 # same code owners.
131 self._seed = random.getrandbits(30)
132
Gavin Make0fee9f2022-08-10 23:41:55 +0000133 def _FetchOwners(self, path, cache, highest_score_only=False):
Edward Lesmes5e37f6d2021-02-17 23:32:16 +0000134 # Always use slashes as separators.
135 path = path.replace(os.sep, '/')
Gavin Make0fee9f2022-08-10 23:41:55 +0000136 if path not in cache:
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000137 # GetOwnersForFile returns a list of account details sorted by order of
138 # best reviewer for path. If owners have the same score, the order is
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000139 # random, seeded by `self._seed`.
Gavin Make0fee9f2022-08-10 23:41:55 +0000140 data = gerrit_util.GetOwnersForFile(self._host,
141 self._project,
142 self._branch,
143 path,
144 resolve_all_users=False,
145 highest_score_only=highest_score_only,
146 seed=self._seed)
147 cache[path] = [
148 d['account']['email'] for d in data['code_owners']
149 if 'account' in d and 'email' in d['account']
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000150 ]
Gavin Mak7d690052021-02-25 19:14:22 +0000151 # If owned_by_all_users is true, add everyone as an owner at the end of
152 # the owners list.
153 if data.get('owned_by_all_users', False):
Gavin Make0fee9f2022-08-10 23:41:55 +0000154 cache[path].append(self.EVERYONE)
155 return cache[path]
156
157 def ListOwners(self, path):
158 return self._FetchOwners(path, self._owners_cache)
159
160 def ListBestOwners(self, path):
161 return self._FetchOwners(path,
162 self._best_owners_cache,
163 highest_score_only=True)
164
165 def BatchListBestOwners(self, paths):
166 """List only the higest-scoring owners for a group of files.
167
168 Returns a dictionary {path: [owners]}.
169 """
170 with git_common.ScopedPool(kind='threads') as pool:
171 return dict(
172 pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)), paths))
Edward Lesmes110823b2021-02-05 21:42:27 +0000173
174
Gavin Mak0f1addc2022-09-08 15:26:06 +0000175def GetCodeOwnersClient(host, project, branch):
Edward Lesmes110823b2021-02-05 21:42:27 +0000176 """Get a new OwnersClient.
177
Gavin Mak0f1addc2022-09-08 15:26:06 +0000178 Uses GerritClient and raises an exception if code-owners plugin is not
179 available."""
Edward Lesmes8170c292021-03-19 20:04:43 +0000180 if gerrit_util.IsCodeOwnersEnabledOnHost(host):
Edward Lesmes88f712e2021-03-15 17:55:13 +0000181 return GerritClient(host, project, branch)
Gavin Mak0f1addc2022-09-08 15:26:06 +0000182 raise Exception(
183 'code-owners plugin is not enabled. Ask your host admin to enable it '
184 'on %s. Read more about code-owners at '
185 'https://gerrit.googlesource.com/plugins/code-owners.' % host)