@ -5,6 +5,7 @@
import itertools
import itertools
import os
import os
import random
import random
import threading
import gerrit_util
import gerrit_util
import git_common
import git_common
@ -39,10 +40,6 @@ def _owner_combinations(owners, num_owners):
return reversed ( list ( itertools . combinations ( reversed ( owners ) , num_owners ) ) )
return reversed ( list ( itertools . combinations ( reversed ( owners ) , num_owners ) ) )
class InvalidOwnersConfig ( Exception ) :
pass
class OwnersClient ( object ) :
class OwnersClient ( object ) :
""" Interact with OWNERS files in a repository.
""" Interact with OWNERS files in a repository.
@ -50,46 +47,30 @@ class OwnersClient(object):
Gerrit Code - Owners plugin REST API , and the owners database implemented by
Gerrit Code - Owners plugin REST API , and the owners database implemented by
Depot Tools in owners . py :
Depot Tools in owners . py :
- List all the owners for a chan ge.
- List all the owners for a group of fil es .
- Check if a change has been approved .
- Check if files have been approved .
- Check if the OWNERS configuration in a change is valid .
- Suggest owners for a group of files .
All code should use this class to interact with OWNERS files instead of the
All code should use this class to interact with OWNERS files instead of the
owners database in owners . py
owners database in owners . py
"""
"""
def __init__ ( self , host ) :
def ListOwners ( self , path ) :
self . _host = host
def ListOwnersForFile ( self , project , branch , path ) :
""" List all owners for a file.
""" List all owners for a file.
The returned list is sorted so that better owners appear first .
The returned list is sorted so that better owners appear first .
"""
"""
raise Exception ( ' Not implemented ' )
raise Exception ( ' Not implemented ' )
def BatchListOwners ( self , project , branch , paths ) :
def BatchListOwners ( self , paths ) :
""" Returns a dictionary { path: [owners]}. """
""" List all owners for a group of files.
with git_common . ScopedPool ( kind = ' threads ' ) as pool :
return dict ( pool . imap_unordered (
lambda p : ( p , self . ListOwnersForFile ( project , branch , p ) ) , paths ) )
def GetChangeApprovalStatus ( self , change_id ) :
""" Check the approval status for the latest revision_id in a change.
Returns a map of path to approval status , where the status can be one of :
Returns a dictionary { path : [ owners ] } .
- APPROVED : An owner of the file has reviewed the change .
- PENDING : An owner of the file has been added as a reviewer , but no owner
has approved .
- INSUFFICIENT_REVIEWERS : No owner of the file has been added as a reviewer .
"""
"""
raise Exception ( ' Not implemented ' )
with git_common . ScopedPool ( kind = ' threads ' ) as pool :
return dict ( pool . imap_unordered (
def ValidateOwnersConfig ( self , change_id ) :
lambda p : ( p , self . ListOwners ( p ) ) , paths ) )
""" Check if the owners configuration in a change is valid. """
raise Exception ( ' Not implemented ' )
def GetFilesApprovalStatus (
def GetFilesApprovalStatus ( self , paths , approvers , reviewers ) :
self , project , branch , paths , approvers , reviewers ) :
""" Check the approval status for the given paths.
""" Check the approval status for the given paths.
Utility method to check for approval status when a change has not yet been
Utility method to check for approval status when a change has not yet been
@ -100,22 +81,23 @@ class OwnersClient(object):
approvers = set ( approvers )
approvers = set ( approvers )
reviewers = set ( reviewers )
reviewers = set ( reviewers )
status = { }
status = { }
for path in paths :
owners_by_path = self . BatchListOwners ( paths )
path_owners = set ( self . ListOwnersForFile ( project , branch , path ) )
for path , owners in owners_by_path . items ( ) :
if path_owners . intersection ( approvers ) :
owners = set ( owners )
if owners . intersection ( approvers ) :
status [ path ] = APPROVED
status [ path ] = APPROVED
elif path_ owners. intersection ( reviewers ) :
elif owners. intersection ( reviewers ) :
status [ path ] = PENDING
status [ path ] = PENDING
else :
else :
status [ path ] = INSUFFICIENT_REVIEWERS
status [ path ] = INSUFFICIENT_REVIEWERS
return status
return status
def SuggestOwners ( self , p roject, branch , p aths) :
def SuggestOwners ( self , p aths) :
""" Suggest a set of owners for the given paths. """
""" Suggest a set of owners for the given paths. """
paths_by_owner = { }
paths_by_owner = { }
score_by_owner = { }
score_by_owner = { }
for path in paths :
owners_by_path = self . BatchListOwners ( paths )
owners = self . ListOwnersForFile ( project , branch , path )
for path , owners in owners_by_path . items ( ) :
for i , owner in enumerate ( owners ) :
for i , owner in enumerate ( owners ) :
paths_by_owner . setdefault ( owner , set ( ) ) . add ( path )
paths_by_owner . setdefault ( owner , set ( ) ) . add ( path )
# Gerrit API lists owners of a path sorted by an internal score, so
# Gerrit API lists owners of a path sorted by an internal score, so
@ -124,8 +106,10 @@ class OwnersClient(object):
# paths.
# paths.
score_by_owner [ owner ] = min ( i , score_by_owner . get ( owner , i ) )
score_by_owner [ owner ] = min ( i , score_by_owner . get ( owner , i ) )
# Sort owners by their score.
# Sort owners by their score. Randomize order of owners with same score.
owners = sorted ( score_by_owner , key = lambda o : score_by_owner [ o ] )
owners = sorted (
score_by_owner ,
key = lambda o : ( score_by_owner [ o ] , random . random ( ) ) )
# Select the minimum number of owners that can approve all paths.
# Select the minimum number of owners that can approve all paths.
# We start at 2 to avoid sending all changes that require multiple reviewers
# We start at 2 to avoid sending all changes that require multiple reviewers
@ -139,19 +123,22 @@ class OwnersClient(object):
for selected in _owner_combinations ( owners , num_owners ) :
for selected in _owner_combinations ( owners , num_owners ) :
covered = set . union ( * ( paths_by_owner [ o ] for o in selected ) )
covered = set . union ( * ( paths_by_owner [ o ] for o in selected ) )
if len ( covered ) == len ( paths ) :
if len ( covered ) == len ( paths ) :
return selected
return list ( selected )
class DepotToolsClient ( OwnersClient ) :
class DepotToolsClient ( OwnersClient ) :
""" Implement OwnersClient using owners.py Database. """
""" Implement OwnersClient using owners.py Database. """
def __init__ ( self , host , root , branch , fopen = open , os_path = os . path ) :
def __init__ ( self , root , branch , fopen = open , os_path = os . path ) :
super ( DepotToolsClient , self ) . __init__ ( host )
super ( DepotToolsClient , self ) . __init__ ( )
self . _root = root
self . _root = root
self . _branch = branch
self . _fopen = fopen
self . _fopen = fopen
self . _os_path = os_path
self . _os_path = os_path
self . _branch = branch
self . _db = owners_db . Database ( root , fopen , os_path )
self . _db = owners_db . Database ( root , fopen , os_path )
self . _db . override_files = self . _GetOriginalOwnersFiles ( )
self . _db . override_files = self . _GetOriginalOwnersFiles ( )
self . _db_lock = threading . Lock ( )
def _GetOriginalOwnersFiles ( self ) :
def _GetOriginalOwnersFiles ( self ) :
return {
return {
@ -160,57 +147,33 @@ class DepotToolsClient(OwnersClient):
if os . path . basename ( f ) == ' OWNERS '
if os . path . basename ( f ) == ' OWNERS '
}
}
def ListOwnersForFile ( self , _project , _branch , path ) :
def ListOwners ( self , path ) :
# all_possible_owners returns a dict {owner: [(path, distance)]}. We want to
# all_possible_owners is not thread safe.
# return a list of owners sorted by increasing distance.
with self . _db_lock :
# all_possible_owners returns a dict {owner: [(path, distance)]}. We want
# to return a list of owners sorted by increasing distance.
distance_by_owner = self . _db . all_possible_owners ( [ path ] , None )
distance_by_owner = self . _db . all_possible_owners ( [ path ] , None )
# We add a small random number to the distance, so that owners at the same
# We add a small random number to the distance, so that owners at the same
# distance are returned in random order to avoid overloading those who would
# distance are returned in random order to avoid overloading those who
# appear first.
# would appear first.
return sorted (
return sorted (
distance_by_owner ,
distance_by_owner ,
key = lambda o : distance_by_owner [ o ] [ 0 ] [ 1 ] + random . random ( ) )
key = lambda o : distance_by_owner [ o ] [ 0 ] [ 1 ] + random . random ( ) )
def GetChangeApprovalStatus ( self , change_id ) :
data = gerrit_util . GetChange (
self . _host , change_id ,
[ ' DETAILED_ACCOUNTS ' , ' DETAILED_LABELS ' , ' CURRENT_FILES ' ,
' CURRENT_REVISION ' ] )
reviewers = [ r [ ' email ' ] for r in data [ ' reviewers ' ] [ ' REVIEWER ' ] ]
# Get reviewers that have approved this change
label = data [ ' labels ' ] [ ' Code-Review ' ]
max_value = max ( int ( v ) for v in label [ ' values ' ] )
approvers = [ v [ ' email ' ] for v in label [ ' all ' ] if v [ ' value ' ] == max_value ]
files = data [ ' revisions ' ] [ data [ ' current_revision ' ] ] [ ' files ' ]
return self . GetFilesApprovalStatus ( None , None , files , approvers , reviewers )
def ValidateOwnersConfig ( self , change_id ) :
data = gerrit_util . GetChange (
self . _host , change_id ,
[ ' DETAILED_ACCOUNTS ' , ' DETAILED_LABELS ' , ' CURRENT_FILES ' ,
' CURRENT_REVISION ' ] )
files = data [ ' revisions ' ] [ data [ ' current_revision ' ] ] [ ' files ' ]
db = owners_db . Database ( self . _root , self . _fopen , self . _os_path )
try :
db . load_data_needed_for (
[ f for f in files if os . path . basename ( f ) == ' OWNERS ' ] )
except Exception as e :
raise InvalidOwnersConfig ( ' Error parsing OWNERS files: \n %s ' % e )
class GerritClient ( OwnersClient ) :
class GerritClient ( OwnersClient ) :
""" Implement OwnersClient using OWNERS REST API. """
""" Implement OwnersClient using OWNERS REST API. """
def __init__ ( self , host ) :
def __init__ ( self , host , project , branch ) :
super ( GerritClient , self ) . __init__ ( host )
super ( GerritClient , self ) . __init__ ( )
self . _host = host
self . _project = project
self . _branch = branch
def ListOwnersForFile ( self , project , branch , path ) :
def ListOwners ( self , path ) :
# GetOwnersForFile returns a list of account details sorted by order of
# GetOwnersForFile returns a list of account details sorted by order of
# best reviewer for path. If code owners have the same score, the order is
# best reviewer for path. If code owners have the same score, the order is
# random.
# random.
data = gerrit_util . GetOwnersForFile ( self . _host , project , branch , path )
data = gerrit_util . GetOwnersForFile (
self . _host , self . _project , self . _branch , path )
return [ d [ ' account ' ] [ ' email ' ] for d in data ]
return [ d [ ' account ' ] [ ' email ' ] for d in data ]