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 |