owners_client.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. # 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. import os
  5. import random
  6. import gerrit_util
  7. import git_common
  8. class OwnersClient(object):
  9. """Interact with OWNERS files in a repository.
  10. This class allows you to interact with OWNERS files in a repository both the
  11. Gerrit Code-Owners plugin REST API, and the owners database implemented by
  12. Depot Tools in owners.py:
  13. - List all the owners for a group of files.
  14. - Check if files have been approved.
  15. - Suggest owners for a group of files.
  16. All code should use this class to interact with OWNERS files instead of the
  17. owners database in owners.py
  18. """
  19. # '*' means that everyone can approve.
  20. EVERYONE = '*'
  21. # Possible status of a file.
  22. # - INSUFFICIENT_REVIEWERS: The path needs owners approval, but none of its
  23. # owners is currently a reviewer of the change.
  24. # - PENDING: An owner of this path has been added as reviewer, but approval
  25. # has not been given yet.
  26. # - APPROVED: The path has been approved by an owner.
  27. APPROVED = 'APPROVED'
  28. PENDING = 'PENDING'
  29. INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS'
  30. def ListOwners(self, path):
  31. """List all owners for a file.
  32. The returned list is sorted so that better owners appear first.
  33. """
  34. raise Exception('Not implemented')
  35. def BatchListOwners(self, paths):
  36. """List all owners for a group of files.
  37. Returns a dictionary {path: [owners]}.
  38. """
  39. if not paths:
  40. return dict()
  41. nproc = min(gerrit_util.MAX_CONCURRENT_CONNECTION, len(paths))
  42. with git_common.ScopedPool(nproc, kind='threads') as pool:
  43. return dict(
  44. pool.imap_unordered(lambda p: (p, self.ListOwners(p)), paths))
  45. def GetFilesApprovalStatus(self, paths, approvers, reviewers):
  46. """Check the approval status for the given paths.
  47. Utility method to check for approval status when a change has not yet
  48. been created, given reviewers and approvers.
  49. See GetChangeApprovalStatus for description of the returned value.
  50. """
  51. approvers = set(approvers)
  52. if approvers:
  53. approvers.add(self.EVERYONE)
  54. reviewers = set(reviewers)
  55. if reviewers:
  56. reviewers.add(self.EVERYONE)
  57. status = {}
  58. owners_by_path = self.BatchListOwners(paths)
  59. for path, owners in owners_by_path.items():
  60. owners = set(owners)
  61. if owners.intersection(approvers):
  62. status[path] = self.APPROVED
  63. elif owners.intersection(reviewers):
  64. status[path] = self.PENDING
  65. else:
  66. status[path] = self.INSUFFICIENT_REVIEWERS
  67. return status
  68. def ScoreOwners(self, paths, exclude=None):
  69. """Get sorted list of owners for the given paths."""
  70. if not paths:
  71. return []
  72. exclude = exclude or []
  73. owners = []
  74. queues = self.BatchListOwners(paths).values()
  75. for i in range(max(len(q) for q in queues)):
  76. for q in queues:
  77. if i < len(q) and q[i] not in owners and q[i] not in exclude:
  78. owners.append(q[i])
  79. return owners
  80. def SuggestOwners(self, paths, exclude=None):
  81. """Suggest a set of owners for the given paths."""
  82. exclude = exclude or []
  83. paths_by_owner = {}
  84. owners_by_path = self.BatchListOwners(paths)
  85. for path, owners in owners_by_path.items():
  86. for owner in owners:
  87. paths_by_owner.setdefault(owner, set()).add(path)
  88. selected = []
  89. missing = set(paths)
  90. for owner in self.ScoreOwners(paths, exclude=exclude):
  91. missing_len = len(missing)
  92. missing.difference_update(paths_by_owner[owner])
  93. if missing_len > len(missing):
  94. selected.append(owner)
  95. if not missing:
  96. break
  97. return selected
  98. def SuggestMinimalOwners(self,
  99. paths: list[str],
  100. exclude: list[str] = None) -> list[str]:
  101. """
  102. Suggest a set of owners for the given paths. Never return an owner in
  103. the |exclude| list.
  104. Aims to provide only one, but will provide more if it's unable to
  105. find a common owner.
  106. """
  107. exclude = exclude or []
  108. owners_by_path = self.BatchListOwners(paths)
  109. if not owners_by_path:
  110. return []
  111. common_owners = set(owners_by_path.popitem()[1]) - set(exclude)
  112. for _, owners in owners_by_path.items():
  113. common_owners = common_owners.intersection(set(owners))
  114. if not common_owners:
  115. # This likely means some of the files had `noparent` set.
  116. # Fall back to the default suggestion algorithm, which accounts
  117. # for noparent but is liable to return many different owners
  118. return self.SuggestOwners(paths, exclude)
  119. # Return an arbitrary common owner, preferring those with a good score
  120. sorted_common_owners = [
  121. owner for owner in self.ScoreOwners(paths, exclude=exclude)
  122. if owner in common_owners
  123. ]
  124. # Return a singleton list so this function has a consistent return type
  125. return sorted_common_owners[:1]
  126. class GerritClient(OwnersClient):
  127. """Implement OwnersClient using OWNERS REST API."""
  128. def __init__(self, host, project, branch):
  129. super(GerritClient, self).__init__()
  130. self._host = host
  131. self._project = project
  132. self._branch = branch
  133. self._owners_cache = {}
  134. self._best_owners_cache = {}
  135. # Seed used by Gerrit to shuffle code owners that have the same score.
  136. # Can be used to make the sort order stable across several requests,
  137. # e.g. to get the same set of random code owners for different file
  138. # paths that have the same code owners.
  139. self._seed = random.getrandbits(30)
  140. def _FetchOwners(self, path, cache, highest_score_only=False):
  141. # Always use slashes as separators.
  142. path = path.replace(os.sep, '/')
  143. if path not in cache:
  144. # GetOwnersForFile returns a list of account details sorted by order
  145. # of best reviewer for path. If owners have the same score, the
  146. # order is random, seeded by `self._seed`.
  147. data = gerrit_util.GetOwnersForFile(
  148. self._host,
  149. self._project,
  150. self._branch,
  151. path,
  152. resolve_all_users=False,
  153. highest_score_only=highest_score_only,
  154. seed=self._seed)
  155. cache[path] = [
  156. d['account']['email'] for d in data['code_owners']
  157. if 'account' in d and 'email' in d['account']
  158. ]
  159. # If owned_by_all_users is true, add everyone as an owner at the end
  160. # of the owners list.
  161. if data.get('owned_by_all_users', False):
  162. cache[path].append(self.EVERYONE)
  163. return cache[path]
  164. def ListOwners(self, path):
  165. return self._FetchOwners(path, self._owners_cache)
  166. def ListBestOwners(self, path):
  167. return self._FetchOwners(path,
  168. self._best_owners_cache,
  169. highest_score_only=True)
  170. def BatchListBestOwners(self, paths):
  171. """List only the higest-scoring owners for a group of files.
  172. Returns a dictionary {path: [owners]}.
  173. """
  174. with git_common.ScopedPool(kind='threads') as pool:
  175. return dict(
  176. pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)),
  177. paths))
  178. def GetCodeOwnersClient(host, project, branch):
  179. """Get a new OwnersClient.
  180. Uses GerritClient and raises an exception if code-owners plugin is not
  181. available."""
  182. if gerrit_util.IsCodeOwnersEnabledOnHost(host):
  183. return GerritClient(host, project, branch)
  184. raise Exception(
  185. 'code-owners plugin is not enabled. Ask your host admin to enable it '
  186. 'on %s. Read more about code-owners at '
  187. 'https://chromium-review.googlesource.com/'
  188. 'plugins/code-owners/Documentation/index.html.' % host)