Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(281)

Side by Side Diff: tools/checkteamtags/checkteamtags.py

Issue 2698813008: [OWNERS tags] Check against multiple teams per component on presubmit (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tools/checkteamtags/checkteamtags_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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())
OLDNEW
« no previous file with comments | « no previous file | tools/checkteamtags/checkteamtags_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698