blob: e70225c5bcc6978eff5ad19a0855280a623f1fd5 [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 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
Edward Lesmesb4f42262020-11-10 23:41:35 +0000120
121class DepotToolsClient(OwnersClient):
122 """Implement OwnersClient using owners.py Database."""
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000123 def __init__(self, root, branch, fopen=open, os_path=os.path):
124 super(DepotToolsClient, self).__init__()
125
Edward Lesmesb4f42262020-11-10 23:41:35 +0000126 self._root = root
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000127 self._branch = branch
Edward Lesmesb4721682020-11-19 22:59:57 +0000128 self._fopen = fopen
129 self._os_path = os_path
Edward Lesmes82b992a2021-01-11 23:24:55 +0000130 self._db = None
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000131 self._db_lock = threading.Lock()
Edward Lesmes829ce022020-11-18 18:30:31 +0000132
Edward Lesmes82b992a2021-01-11 23:24:55 +0000133 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
Edward Lesmes829ce022020-11-18 18:30:31 +0000139 def _GetOriginalOwnersFiles(self):
140 return {
Edward Lesmes1eaaab52021-03-02 23:52:54 +0000141 f: scm.GIT.GetOldContents(self._root, f, self._branch).splitlines()
142 for _, f in scm.GIT.CaptureStatus(self._root, self._branch)
Edward Lesmesd4e6fb62020-11-17 00:17:58 +0000143 if os.path.basename(f) == 'OWNERS'
Edward Lesmes829ce022020-11-18 18:30:31 +0000144 }
Edward Lesmesb4f42262020-11-10 23:41:35 +0000145
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000146 def ListOwners(self, path):
147 # all_possible_owners is not thread safe.
148 with self._db_lock:
Edward Lesmes82b992a2021-01-11 23:24:55 +0000149 self._ensure_db()
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000150 # 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)
Gavin Makd36dbbd2021-01-25 19:34:58 +0000153 # 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.
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000156 return sorted(
157 distance_by_owner,
158 key=lambda o: distance_by_owner[o][0][1] + random.random())
Gavin Makc94b21d2020-12-10 20:27:32 +0000159
160
161class 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 = {}
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000170
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000171 # Seed used by Gerrit to shuffle code owners that have the same score. Can
172 # be used to make the sort order stable across several requests, e.g. to get
173 # the same set of random code owners for different file paths that have the
174 # same code owners.
175 self._seed = random.getrandbits(30)
176
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +0000177 def ListOwners(self, path):
Edward Lesmes5e37f6d2021-02-17 23:32:16 +0000178 # Always use slashes as separators.
179 path = path.replace(os.sep, '/')
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000180 if path not in self._owners_cache:
181 # GetOwnersForFile returns a list of account details sorted by order of
182 # best reviewer for path. If owners have the same score, the order is
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000183 # random, seeded by `self._seed`.
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000184 data = gerrit_util.GetOwnersForFile(
Gavin Mak7d690052021-02-25 19:14:22 +0000185 self._host, self._project, self._branch, path,
Edward Lesmes23c3bdc2021-03-11 20:37:32 +0000186 resolve_all_users=False, seed=self._seed)
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000187 self._owners_cache[path] = [
188 d['account']['email']
189 for d in data['code_owners']
Gavin Mak7d690052021-02-25 19:14:22 +0000190 if 'account' in d and 'email' in d['account']
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000191 ]
Gavin Mak7d690052021-02-25 19:14:22 +0000192 # If owned_by_all_users is true, add everyone as an owner at the end of
193 # the owners list.
194 if data.get('owned_by_all_users', False):
195 self._owners_cache[path].append(self.EVERYONE)
Edward Lesmes0d1bdb22021-02-16 21:27:04 +0000196 return self._owners_cache[path]
Edward Lesmes110823b2021-02-05 21:42:27 +0000197
198
Edward Lesmes1eaaab52021-03-02 23:52:54 +0000199def GetCodeOwnersClient(root, upstream, host, project, branch):
Edward Lesmes110823b2021-02-05 21:42:27 +0000200 """Get a new OwnersClient.
201
202 Defaults to GerritClient, and falls back to DepotToolsClient if code-owners
203 plugin is not available."""
Edward Lesmes88f712e2021-03-15 17:55:13 +0000204 if gerrit_util.IsCodeOwnersEnabled(host):
205 return GerritClient(host, project, branch)
Edward Lesmes1eaaab52021-03-02 23:52:54 +0000206 return DepotToolsClient(root, upstream)