owners_client.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. with git_common.ScopedPool(kind='threads') as pool:
  40. return dict(pool.imap_unordered(
  41. lambda p: (p, self.ListOwners(p)), paths))
  42. def GetFilesApprovalStatus(self, paths, approvers, reviewers):
  43. """Check the approval status for the given paths.
  44. Utility method to check for approval status when a change has not yet been
  45. created, given reviewers and approvers.
  46. See GetChangeApprovalStatus for description of the returned value.
  47. """
  48. approvers = set(approvers)
  49. if approvers:
  50. approvers.add(self.EVERYONE)
  51. reviewers = set(reviewers)
  52. if reviewers:
  53. reviewers.add(self.EVERYONE)
  54. status = {}
  55. owners_by_path = self.BatchListOwners(paths)
  56. for path, owners in owners_by_path.items():
  57. owners = set(owners)
  58. if owners.intersection(approvers):
  59. status[path] = self.APPROVED
  60. elif owners.intersection(reviewers):
  61. status[path] = self.PENDING
  62. else:
  63. status[path] = self.INSUFFICIENT_REVIEWERS
  64. return status
  65. def ScoreOwners(self, paths, exclude=None):
  66. """Get sorted list of owners for the given paths."""
  67. if not paths:
  68. return []
  69. exclude = exclude or []
  70. owners = []
  71. queues = self.BatchListOwners(paths).values()
  72. for i in range(max(len(q) for q in queues)):
  73. for q in queues:
  74. if i < len(q) and q[i] not in owners and q[i] not in exclude:
  75. owners.append(q[i])
  76. return owners
  77. def SuggestOwners(self, paths, exclude=None):
  78. """Suggest a set of owners for the given paths."""
  79. exclude = exclude or []
  80. paths_by_owner = {}
  81. owners_by_path = self.BatchListOwners(paths)
  82. for path, owners in owners_by_path.items():
  83. for owner in owners:
  84. paths_by_owner.setdefault(owner, set()).add(path)
  85. selected = []
  86. missing = set(paths)
  87. for owner in self.ScoreOwners(paths, exclude=exclude):
  88. missing_len = len(missing)
  89. missing.difference_update(paths_by_owner[owner])
  90. if missing_len > len(missing):
  91. selected.append(owner)
  92. if not missing:
  93. break
  94. return selected
  95. class GerritClient(OwnersClient):
  96. """Implement OwnersClient using OWNERS REST API."""
  97. def __init__(self, host, project, branch):
  98. super(GerritClient, self).__init__()
  99. self._host = host
  100. self._project = project
  101. self._branch = branch
  102. self._owners_cache = {}
  103. self._best_owners_cache = {}
  104. # Seed used by Gerrit to shuffle code owners that have the same score. Can
  105. # be used to make the sort order stable across several requests, e.g. to get
  106. # the same set of random code owners for different file paths that have the
  107. # same code owners.
  108. self._seed = random.getrandbits(30)
  109. def _FetchOwners(self, path, cache, highest_score_only=False):
  110. # Always use slashes as separators.
  111. path = path.replace(os.sep, '/')
  112. if path not in cache:
  113. # GetOwnersForFile returns a list of account details sorted by order of
  114. # best reviewer for path. If owners have the same score, the order is
  115. # random, seeded by `self._seed`.
  116. data = gerrit_util.GetOwnersForFile(self._host,
  117. self._project,
  118. self._branch,
  119. path,
  120. resolve_all_users=False,
  121. highest_score_only=highest_score_only,
  122. seed=self._seed)
  123. cache[path] = [
  124. d['account']['email'] for d in data['code_owners']
  125. if 'account' in d and 'email' in d['account']
  126. ]
  127. # If owned_by_all_users is true, add everyone as an owner at the end of
  128. # the owners list.
  129. if data.get('owned_by_all_users', False):
  130. cache[path].append(self.EVERYONE)
  131. return cache[path]
  132. def ListOwners(self, path):
  133. return self._FetchOwners(path, self._owners_cache)
  134. def ListBestOwners(self, path):
  135. return self._FetchOwners(path,
  136. self._best_owners_cache,
  137. highest_score_only=True)
  138. def BatchListBestOwners(self, paths):
  139. """List only the higest-scoring owners for a group of files.
  140. Returns a dictionary {path: [owners]}.
  141. """
  142. with git_common.ScopedPool(kind='threads') as pool:
  143. return dict(
  144. pool.imap_unordered(lambda p: (p, self.ListBestOwners(p)), paths))
  145. def GetCodeOwnersClient(host, project, branch):
  146. """Get a new OwnersClient.
  147. Uses GerritClient and raises an exception if code-owners plugin is not
  148. available."""
  149. if gerrit_util.IsCodeOwnersEnabledOnHost(host):
  150. return GerritClient(host, project, branch)
  151. raise Exception(
  152. 'code-owners plugin is not enabled. Ask your host admin to enable it '
  153. 'on %s. Read more about code-owners at '
  154. 'https://gerrit.googlesource.com/plugins/code-owners.' % host)