Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2017 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2017 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Makes sure OWNERS files have consistent TEAM and COMPONENT tags.""" | 6 """Makes sure OWNERS files have consistent TEAM and COMPONENT tags.""" |
| 7 | 7 |
| 8 | 8 |
| 9 import json | 9 import json |
| 10 import logging | 10 import logging |
| 11 import optparse | 11 import optparse |
| 12 import os | 12 import os |
| 13 import sys | 13 import sys |
| 14 import urllib2 | |
| 15 | |
| 16 from collections import defaultdict | |
| 17 | |
| 18 from owners_file_tags import parse | |
| 14 | 19 |
| 15 | 20 |
| 16 def check_owners(root, owners_path): | 21 DEFAULT_MAPPING_URL = \ |
| 17 """Component and Team check in OWNERS files. crbug.com/667954""" | 22 'https://storage.googleapis.com/chromium-owners/component_map.json' |
| 23 | |
| 24 | |
| 25 def rel_and_full_paths(root, owners_path): | |
| 18 if root: | 26 if root: |
| 19 full_path = os.path.join(root, owners_path) | 27 full_path = os.path.join(root, owners_path) |
| 20 rel_path = owners_path | 28 rel_path = owners_path |
| 21 else: | 29 else: |
| 22 full_path = os.path.abspath(owners_path) | 30 full_path = os.path.abspath(owners_path) |
| 23 rel_path = os.path.relpath(owners_path) | 31 rel_path = os.path.relpath(owners_path) |
| 32 return rel_path, full_path | |
| 24 | 33 |
| 34 | |
| 35 def validate_mappings(options, args): | |
| 36 """Ensure team/component mapping remains consistent after patch. | |
| 37 | |
| 38 The main purpose of this check is to prevent new and edited OWNERS files | |
| 39 introduce multiple teams for the same component. | |
| 40 | |
| 41 Args: | |
| 42 options: Command line options from optparse | |
| 43 args: List of paths to affected OWNERS files | |
| 44 """ | |
| 45 mappings_file = json.load(urllib2.urlopen(options.current_mapping_url)) | |
| 46 | |
| 47 # Convert dir -> component, component -> team to dir -> (team, component) | |
| 48 current_mappings = {} | |
| 49 for dir_name in mappings_file['dir-to-component'].keys(): | |
| 50 component = mappings_file['dir-to-component'].get(dir_name) | |
| 51 if component: | |
| 52 team = mappings_file['component-to-team'].get(component) | |
| 53 else: | |
| 54 team = None | |
| 55 current_mappings[dir_name] = (team, component) | |
|
ymzhang1
2017/02/17 21:00:02
Maybe we could save dir->(team, component) in comp
| |
| 56 | |
| 57 # Extract dir -> (team, component) for affected files | |
| 58 affected = {} | |
| 59 deleted = [] | |
| 60 for f in args: | |
| 61 rel, full = rel_and_full_paths(options.root, f) | |
| 62 if os.path.exists(full): | |
| 63 affected[os.path.dirname(rel)] = parse(full) | |
| 64 else: | |
| 65 deleted.append(os.path.dirname(rel)) | |
| 66 for d in deleted: | |
| 67 current_mappings.pop(d, None) | |
| 68 current_mappings.update(affected) | |
| 69 | |
| 70 #Ensure internal consistency of modified mappings. | |
| 71 new_dir_to_component = {} | |
| 72 new_component_to_team = {} | |
| 73 team_to_dir = defaultdict(list) | |
| 74 errors = {} | |
| 75 for dir_name, tags in current_mappings.iteritems(): | |
| 76 team, component = tags | |
| 77 if component: | |
| 78 new_dir_to_component[dir_name] = component | |
| 79 if team: | |
| 80 team_to_dir[team].append(dir_name) | |
| 81 if component and team: | |
| 82 if new_component_to_team.setdefault(component, team) != team: | |
| 83 if component not in errors: | |
| 84 errors[component] = set([new_component_to_team[component], team]) | |
| 85 else: | |
| 86 errors[component].add(team) | |
| 87 | |
| 88 result = [] | |
| 89 for component, teams in errors.iteritems(): | |
| 90 error_message = 'The component "%s" has more than one team: ' % component | |
| 91 team_details = [] | |
| 92 for team in teams: | |
| 93 team_details.append('%(team)s is used in %(paths)s' % { | |
| 94 'team': team, | |
| 95 'paths': ', '.join(team_to_dir[team]), | |
| 96 }) | |
| 97 error_message += '; '.join(team_details) | |
| 98 result.append({ | |
| 99 'error': error_message, | |
| 100 'full_path': | |
| 101 ' '.join(['%s/OWNERS' % d | |
| 102 for d, c in new_dir_to_component.iteritems() | |
| 103 if c == component and d in affected.keys()]) | |
| 104 }) | |
| 105 return result | |
| 106 | |
| 107 | |
| 108 def check_owners(rel_path, full_path): | |
| 109 """Component and Team check in OWNERS files. crbug.com/667954""" | |
| 25 def result_dict(error): | 110 def result_dict(error): |
| 26 return { | 111 return { |
| 27 'error': error, | 112 'error': error, |
| 28 'full_path': full_path, | 113 'full_path': full_path, |
| 29 'rel_path': rel_path, | 114 'rel_path': rel_path, |
| 30 } | 115 } |
| 31 | 116 |
| 117 if not os.path.exists(full_path): | |
| 118 return | |
| 119 | |
| 32 with open(full_path) as f: | 120 with open(full_path) as f: |
| 33 owners_file_lines = f.readlines() | 121 owners_file_lines = f.readlines() |
| 34 | 122 |
| 35 component_entries = [l for l in owners_file_lines if l.split()[:2] == | 123 component_entries = [l for l in owners_file_lines if l.split()[:2] == |
| 36 ['#', 'COMPONENT:']] | 124 ['#', 'COMPONENT:']] |
| 37 team_entries = [l for l in owners_file_lines if l.split()[:2] == | 125 team_entries = [l for l in owners_file_lines if l.split()[:2] == |
| 38 ['#', 'TEAM:']] | 126 ['#', 'TEAM:']] |
| 39 if len(component_entries) > 1: | 127 if len(component_entries) > 1: |
| 40 return result_dict('Contains more than one component per directory') | 128 return result_dict('Contains more than one component per directory') |
| 41 if len(team_entries) > 1: | 129 if len(team_entries) > 1: |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 82 parser = optparse.OptionParser(usage=usage) | 170 parser = optparse.OptionParser(usage=usage) |
| 83 parser.add_option( | 171 parser.add_option( |
| 84 '--root', help='Specifies the repository root.') | 172 '--root', help='Specifies the repository root.') |
| 85 parser.add_option( | 173 parser.add_option( |
| 86 '-v', '--verbose', action='count', default=0, help='Print debug logging') | 174 '-v', '--verbose', action='count', default=0, help='Print debug logging') |
| 87 parser.add_option( | 175 parser.add_option( |
| 88 '--bare', | 176 '--bare', |
| 89 action='store_true', | 177 action='store_true', |
| 90 default=False, | 178 default=False, |
| 91 help='Prints the bare filename triggering the checks') | 179 help='Prints the bare filename triggering the checks') |
| 180 parser.add_option( | |
| 181 '--current_mapping_url', default=DEFAULT_MAPPING_URL, | |
| 182 help='URL for existing dir/component and component/team mapping') | |
| 92 parser.add_option('--json', help='Path to JSON output file') | 183 parser.add_option('--json', help='Path to JSON output file') |
| 93 options, args = parser.parse_args() | 184 options, args = parser.parse_args() |
| 94 | 185 |
| 95 levels = [logging.ERROR, logging.INFO, logging.DEBUG] | 186 levels = [logging.ERROR, logging.INFO, logging.DEBUG] |
| 96 logging.basicConfig(level=levels[min(len(levels) - 1, options.verbose)]) | 187 logging.basicConfig(level=levels[min(len(levels) - 1, options.verbose)]) |
| 97 | 188 |
| 98 errors = filter(None, [check_owners(options.root, f) for f in args]) | 189 errors = filter(None, [check_owners(*rel_and_full_paths(options.root, f)) |
| 190 for f in args]) | |
| 191 | |
| 192 if not errors: | |
| 193 errors += validate_mappings(options, args) or [] | |
| 99 | 194 |
| 100 if options.json: | 195 if options.json: |
| 101 with open(options.json, 'w') as f: | 196 with open(options.json, 'w') as f: |
| 102 json.dump(errors, f) | 197 json.dump(errors, f) |
| 103 | 198 |
| 104 if errors: | 199 if errors: |
| 105 if options.bare: | 200 if options.bare: |
| 106 print '\n'.join(e['full_path'] for e in errors) | 201 print '\n'.join(e['full_path'] for e in errors) |
| 107 else: | 202 else: |
| 108 print '\nFAILED\n' | 203 print '\nFAILED\n' |
| 109 print '\n'.join('%s: %s' % (e['full_path'], e['error']) for e in errors) | 204 print '\n'.join('%s: %s' % (e['full_path'], e['error']) for e in errors) |
| 110 return 1 | 205 return 1 |
| 111 if not options.bare: | 206 if not options.bare: |
| 112 print '\nSUCCESS\n' | 207 print '\nSUCCESS\n' |
| 113 return 0 | 208 return 0 |
| 114 | 209 |
| 115 | 210 |
| 116 if '__main__' == __name__: | 211 if '__main__' == __name__: |
| 117 sys.exit(main()) | 212 sys.exit(main()) |
| OLD | NEW |