owners_client.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. class GerritClient(OwnersClient):
  99. """Implement OwnersClient using OWNERS REST API."""
  100. def __init__(self, host, project, branch):
  101. super(GerritClient, self).__init__()
  102. self._host = host
  103. self._project = project
  104. self._branch = branch
  105. self._owners_cache = {}
  106. self._best_owners_cache = {}
  107. # Seed used by Gerrit to shuffle code owners that have the same score.
  108. # Can be used to make the sort order stable across several requests,
  109. # e.g. to get the same set of random code owners for different file
  110. # paths that have the same code owners.
  111. self._seed = random.getrandbits(30)
  112. def _FetchOwners(self, path, cache, highest_score_only=False):
  113. # Always use slashes as separators.
  114. path = path.replace(os.sep, '/')
  115. if path not in cache:
  116. # GetOwnersForFile returns a list of account details sorted by order
  117. # of best reviewer for path. If owners have the same score, the
  118. # order is random, seeded by `self._seed`.
  119. data = gerrit_util.GetOwnersForFile(
  120. self._host,
  121. self._project,
  122. self._branch,
  123. path,
  124. resolve_all_users=False,
  125. highest_score_only=highest_score_only,
  126. seed=self._seed)
  127. cache[path] = [
  128. d['account']['email'] for d in data['code_owners']
  129. if 'account' in d and 'email' in d['account']
  130. ]
  131. # If owned_by_all_users is true, add everyone as an owner at the end
  132. # of the owners list.
  133. if data.get('owned_by_all_users', False):
  134. cache[path].append(self.EVERYONE)
  135. return cache[path]
  136. def ListOwners(self, path):
  137. return self._FetchOwners(path, self._owners_cache)
  138. def ListBestOwners(self, path):
  139. return self._FetchOwners(path,
  140. self._best_owners_cache,
  141. highest_score_only=True)
  142. def BatchListBestOwners(self, paths):
  143. """List only the higest-scoring owners for a group of files.
  144. Returns a dictionary {path: [owners]}.
  145. """
  146. with git_common.ScopedPool(kind='threads') as pool:
  147. return dict(
  148. pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)),
  149. paths))
  150. def GetCodeOwnersClient(host, project, branch):
  151. """Get a new OwnersClient.
  152. Uses GerritClient and raises an exception if code-owners plugin is not
  153. available."""
  154. if gerrit_util.IsCodeOwnersEnabledOnHost(host):
  155. return GerritClient(host, project, branch)
  156. raise Exception(
  157. 'code-owners plugin is not enabled. Ask your host admin to enable it '
  158. 'on %s. Read more about code-owners at '
  159. 'https://chromium-review.googlesource.com/'
  160. 'plugins/code-owners/Documentation/index.html.' % host)