blob: 5f948e87eb6965b0388abf4a50067f70eda0f138 [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):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000013 """Interact with OWNERS files in a repository.
Edward Lesmes91bb7502020-11-06 00:50:24 +000014
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 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000026 # '*' means that everyone can approve.
27 EVERYONE = '*'
Edward Lesmes071c3b12021-01-15 19:02:59 +000028
Mike Frysinger124bb8e2023-09-06 05:48:55 +000029 # 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.
35 APPROVED = 'APPROVED'
36 PENDING = 'PENDING'
37 INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
Edward Lesmesc40b2402021-01-12 20:03:11 +000038
Mike Frysinger124bb8e2023-09-06 05:48:55 +000039 def ListOwners(self, path):
40 """List all owners for a file.
Edward Lesmes64e80762020-11-24 19:46:45 +000041
42 The returned list is sorted so that better owners appear first.
43 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000044 raise Exception('Not implemented')
Edward Lesmes91bb7502020-11-06 00:50:24 +000045
Mike Frysinger124bb8e2023-09-06 05:48:55 +000046 def BatchListOwners(self, paths):
47 """List all owners for a group of files.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +000048
49 Returns a dictionary {path: [owners]}.
50 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000051 with git_common.ScopedPool(kind='threads') as pool:
52 return dict(
53 pool.imap_unordered(lambda p: (p, self.ListOwners(p)), paths))
Gavin Mak99399ca2020-12-11 20:56:03 +000054
Mike Frysinger124bb8e2023-09-06 05:48:55 +000055 def GetFilesApprovalStatus(self, paths, approvers, reviewers):
56 """Check the approval status for the given paths.
Edward Lesmese7d18622020-11-19 23:46:17 +000057
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 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000063 approvers = set(approvers)
64 if approvers:
65 approvers.add(self.EVERYONE)
66 reviewers = set(reviewers)
67 if reviewers:
68 reviewers.add(self.EVERYONE)
69 status = {}
70 owners_by_path = self.BatchListOwners(paths)
71 for path, owners in owners_by_path.items():
72 owners = set(owners)
73 if owners.intersection(approvers):
74 status[path] = self.APPROVED
75 elif owners.intersection(reviewers):
76 status[path] = self.PENDING
77 else:
78 status[path] = self.INSUFFICIENT_REVIEWERS
79 return status
Edward Lesmese7d18622020-11-19 23:46:17 +000080
Mike Frysinger124bb8e2023-09-06 05:48:55 +000081 def ScoreOwners(self, paths, exclude=None):
82 """Get sorted list of owners for the given paths."""
83 if not paths:
84 return []
85 exclude = exclude or []
86 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
Mike Frysinger124bb8e2023-09-06 05:48:55 +000094 def SuggestOwners(self, paths, exclude=None):
95 """Suggest a set of owners for the given paths."""
96 exclude = exclude or []
Edward Lesmes23c3bdc2021-03-11 20:37:32 +000097
Mike Frysinger124bb8e2023-09-06 05:48:55 +000098 paths_by_owner = {}
99 owners_by_path = self.BatchListOwners(paths)
100 for path, owners in owners_by_path.items():
101 for owner in owners:
102 paths_by_owner.setdefault(owner, set()).add(path)
Edward Lesmes295dd182020-11-24 23:07:26 +0000103
Mike Frysinger124bb8e2023-09-06 05:48:55 +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
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000114 return selected
115
Edward Lesmes0e2aee72021-02-03 20:12:46 +0000116
Gavin Makc94b21d2020-12-10 20:27:32 +0000117class GerritClient(OwnersClient):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000118 """Implement OwnersClient using OWNERS REST API."""
119 def __init__(self, host, project, branch):
120 super(GerritClient, self).__init__()
Gavin Makc94b21d2020-12-10 20:27:32 +0000121
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000122 self._host = host
123 self._project = project
124 self._branch = branch
125 self._owners_cache = {}
126 self._best_owners_cache = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 # Seed used by Gerrit to shuffle code owners that have the same score.
129 # Can be used to make the sort order stable across several requests,
130 # e.g. to get the same set of random code owners for different file
131 # paths that have the same code owners.
132 self._seed = random.getrandbits(30)
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000133
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000134 def _FetchOwners(self, path, cache, highest_score_only=False):
135 # Always use slashes as separators.
136 path = path.replace(os.sep, '/')
137 if path not in cache:
138 # GetOwnersForFile returns a list of account details sorted by order
139 # of best reviewer for path. If owners have the same score, the
140 # order is random, seeded by `self._seed`.
141 data = gerrit_util.GetOwnersForFile(
142 self._host,
143 self._project,
144 self._branch,
145 path,
146 resolve_all_users=False,
147 highest_score_only=highest_score_only,
148 seed=self._seed)
149 cache[path] = [
150 d['account']['email'] for d in data['code_owners']
151 if 'account' in d and 'email' in d['account']
152 ]
153 # If owned_by_all_users is true, add everyone as an owner at the end
154 # of the owners list.
155 if data.get('owned_by_all_users', False):
156 cache[path].append(self.EVERYONE)
157 return cache[path]
Gavin Make0fee9f2022-08-10 23:41:55 +0000158
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000159 def ListOwners(self, path):
160 return self._FetchOwners(path, self._owners_cache)
Gavin Make0fee9f2022-08-10 23:41:55 +0000161
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000162 def ListBestOwners(self, path):
163 return self._FetchOwners(path,
164 self._best_owners_cache,
165 highest_score_only=True)
Gavin Make0fee9f2022-08-10 23:41:55 +0000166
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000167 def BatchListBestOwners(self, paths):
168 """List only the higest-scoring owners for a group of files.
Gavin Make0fee9f2022-08-10 23:41:55 +0000169
170 Returns a dictionary {path: [owners]}.
171 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000172 with git_common.ScopedPool(kind='threads') as pool:
173 return dict(
174 pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)),
175 paths))
Edward Lesmes110823b2021-02-05 21:42:27 +0000176
177
Gavin Mak0f1addc2022-09-08 15:26:06 +0000178def GetCodeOwnersClient(host, project, branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 """Get a new OwnersClient.
Edward Lesmes110823b2021-02-05 21:42:27 +0000180
Gavin Mak0f1addc2022-09-08 15:26:06 +0000181 Uses GerritClient and raises an exception if code-owners plugin is not
182 available."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000183 if gerrit_util.IsCodeOwnersEnabledOnHost(host):
184 return GerritClient(host, project, branch)
185 raise Exception(
186 'code-owners plugin is not enabled. Ask your host admin to enable it '
187 'on %s. Read more about code-owners at '
188 'https://chromium-review.googlesource.com/'
189 'plugins/code-owners/Documentation/index.html.' % host)