blob: 4b4799c4419e304c919974d5b1e3942b3433b1cd [file] [log] [blame]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001# Copyright (c) 2011 The Chromium OS 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
Dale Curtis2975c432011-05-03 17:25:20 -07005import json
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07006import os
Ryan Cuiec4d6332011-05-02 14:15:25 -07007import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -07008import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07009import subprocess
10
Ryan Cui1562fb82011-05-09 11:01:31 -070011from errors import (VerifyException, HookFailure, PrintErrorForProject,
12 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070013
Ryan Cuiec4d6332011-05-02 14:15:25 -070014
15COMMON_INCLUDED_PATHS = [
16 # C++ and friends
17 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
18 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
19 # Scripts
20 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
21 # No extension at all, note that ALL CAPS files are black listed in
22 # COMMON_EXCLUDED_LIST below.
23 r"(^|.*?[\\\/])[^.]+$",
24 # Other
25 r".*\.java$", r".*\.mk$", r".*\.am$",
26]
27
Ryan Cui1562fb82011-05-09 11:01:31 -070028
Ryan Cuiec4d6332011-05-02 14:15:25 -070029COMMON_EXCLUDED_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -070030 # avoid doing source file checks for kernel
31 r"/src/third_party/kernel/",
32 r"/src/third_party/kernel-next/",
Paul Taysomf8b6e012011-05-09 14:32:42 -070033 r"/src/third_party/ktop/",
34 r"/src/third_party/punybench/",
Ryan Cuiec4d6332011-05-02 14:15:25 -070035 r".*\bexperimental[\\\/].*",
36 r".*\b[A-Z0-9_]{2,}$",
37 r".*[\\\/]debian[\\\/]rules$",
38]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070039
Ryan Cui1562fb82011-05-09 11:01:31 -070040
41# General Helpers
42
Sean Paulba01d402011-05-05 11:36:23 -040043
Ryan Cui72834d12011-05-05 14:51:33 -070044def _run_command(cmd):
45 """Executes the passed in command and returns raw stdout output."""
46 return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
47
Ryan Cui1562fb82011-05-09 11:01:31 -070048
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070049def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -070050 """Returns the absolute path to the repohooks directory."""
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070051 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
Ryan Cui72834d12011-05-05 14:51:33 -070052 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070053
Ryan Cui1562fb82011-05-09 11:01:31 -070054
Ryan Cuiec4d6332011-05-02 14:15:25 -070055def _match_regex_list(subject, expressions):
56 """Try to match a list of regular expressions to a string.
57
58 Args:
59 subject: The string to match regexes on
60 expressions: A list of regular expressions to check for matches with.
61
62 Returns:
63 Whether the passed in subject matches any of the passed in regexes.
64 """
65 for expr in expressions:
66 if (re.search(expr, subject)):
67 return True
68 return False
69
Ryan Cui1562fb82011-05-09 11:01:31 -070070
Ryan Cuiec4d6332011-05-02 14:15:25 -070071def _filter_files(files, include_list, exclude_list=[]):
72 """Filter out files based on the conditions passed in.
73
74 Args:
75 files: list of filepaths to filter
76 include_list: list of regex that when matched with a file path will cause it
77 to be added to the output list unless the file is also matched with a
78 regex in the exclude_list.
79 exclude_list: list of regex that when matched with a file will prevent it
80 from being added to the output list, even if it is also matched with a
81 regex in the include_list.
82
83 Returns:
84 A list of filepaths that contain files matched in the include_list and not
85 in the exclude_list.
86 """
87 filtered = []
88 for f in files:
89 if (_match_regex_list(f, include_list) and
90 not _match_regex_list(f, exclude_list)):
91 filtered.append(f)
92 return filtered
93
Ryan Cuiec4d6332011-05-02 14:15:25 -070094
95# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -070096
97
Ryan Cui4725d952011-05-05 15:41:19 -070098def _get_upstream_branch():
99 """Returns the upstream tracking branch of the current branch.
100
101 Raises:
102 Error if there is no tracking branch
103 """
104 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
105 current_branch = current_branch.replace('refs/heads/', '')
106 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700107 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700108
109 cfg_option = 'branch.' + current_branch + '.%s'
110 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
111 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
112 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700113 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700114
115 return full_upstream.replace('heads', 'remotes/' + remote)
116
Ryan Cui1562fb82011-05-09 11:01:31 -0700117
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700118def _get_diff(commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700119 """Returns the diff for this commit."""
Ryan Cui72834d12011-05-05 14:51:33 -0700120 return _run_command(['git', 'show', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700121
Ryan Cui1562fb82011-05-09 11:01:31 -0700122
Ryan Cuiec4d6332011-05-02 14:15:25 -0700123def _get_file_diff(file, commit):
124 """Returns a list of (linenum, lines) tuples that the commit touched."""
Ryan Cui72834d12011-05-05 14:51:33 -0700125 output = _run_command(['git', 'show', '-p', '--no-ext-diff', commit, file])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700126
127 new_lines = []
128 line_num = 0
129 for line in output.splitlines():
130 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
131 if m:
132 line_num = int(m.groups(1)[0])
133 continue
134 if line.startswith('+') and not line.startswith('++'):
135 new_lines.append((line_num, line[1:]))
136 if not line.startswith('-'):
137 line_num += 1
138 return new_lines
139
Ryan Cui1562fb82011-05-09 11:01:31 -0700140
Ryan Cuiec4d6332011-05-02 14:15:25 -0700141def _get_affected_files(commit):
142 """Returns list of absolute filepaths that were modified/added."""
Ryan Cui72834d12011-05-05 14:51:33 -0700143 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700144 files = []
145 for statusline in output.splitlines():
146 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
147 # Ignore deleted files, and return absolute paths of files
148 if (m.group(1)[0] != 'D'):
149 pwd = os.getcwd()
150 files.append(os.path.join(pwd, m.group(2)))
151 return files
152
Ryan Cui1562fb82011-05-09 11:01:31 -0700153
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700154def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700155 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700156 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700157 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700158
Ryan Cui1562fb82011-05-09 11:01:31 -0700159
Ryan Cuiec4d6332011-05-02 14:15:25 -0700160def _get_commit_desc(commit):
161 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400162 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700163
164
165# Common Hooks
166
Ryan Cui1562fb82011-05-09 11:01:31 -0700167
Ryan Cuiec4d6332011-05-02 14:15:25 -0700168def _check_no_long_lines(project, commit):
169 """Checks that there aren't any lines longer than maxlen characters in any of
170 the text files to be submitted.
171 """
172 MAX_LEN = 80
173
174 errors = []
175 files = _filter_files(_get_affected_files(commit),
176 COMMON_INCLUDED_PATHS,
177 COMMON_EXCLUDED_PATHS)
178
179 for afile in files:
180 for line_num, line in _get_file_diff(afile, commit):
181 # Allow certain lines to exceed the maxlen rule.
182 if (len(line) > MAX_LEN and
183 not 'http://' in line and
184 not 'https://' in line and
185 not line.startswith('#define') and
186 not line.startswith('#include') and
187 not line.startswith('#import') and
188 not line.startswith('#pragma') and
189 not line.startswith('#if') and
190 not line.startswith('#endif')):
191 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
192 if len(errors) == 5: # Just show the first 5 errors.
193 break
194
195 if errors:
196 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700197 return HookFailure(msg, errors)
198
Ryan Cuiec4d6332011-05-02 14:15:25 -0700199
200def _check_no_stray_whitespace(project, commit):
201 """Checks that there is no stray whitespace at source lines end."""
202 errors = []
203 files = _filter_files(_get_affected_files(commit),
204 COMMON_INCLUDED_PATHS,
205 COMMON_EXCLUDED_PATHS)
206
207 for afile in files:
208 for line_num, line in _get_file_diff(afile, commit):
209 if line.rstrip() != line:
210 errors.append('%s, line %s' % (afile, line_num))
211 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700212 return HookFailure('Found line ending with white space in:', errors)
213
Ryan Cuiec4d6332011-05-02 14:15:25 -0700214
215def _check_no_tabs(project, commit):
216 """Checks there are no unexpanded tabs."""
217 TAB_OK_PATHS = [
Doug Anderson0f91cbf2011-05-09 15:56:25 -0700218 r"/src/platform/u-boot-config/",
Ryan Cui31e0c172011-05-04 21:00:45 -0700219 r"/src/third_party/u-boot/",
220 r"/src/third_party/u-boot-next/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700221 r".*\.ebuild$",
222 r".*\.eclass$",
223 r".*/[M|m]akefile$"
224 ]
225
226 errors = []
227 files = _filter_files(_get_affected_files(commit),
228 COMMON_INCLUDED_PATHS,
229 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
230
231 for afile in files:
232 for line_num, line in _get_file_diff(afile, commit):
233 if '\t' in line:
234 errors.append('%s, line %s' % (afile, line_num))
235 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700236 return HookFailure('Found a tab character in:', errors)
237
Ryan Cuiec4d6332011-05-02 14:15:25 -0700238
239def _check_change_has_test_field(project, commit):
240 """Check for a non-empty 'TEST=' field in the commit message."""
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700241 TEST_RE = r'\n\s*TEST\s*=[^\n]*\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700242
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700243 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700244 msg = 'Changelist description needs TEST field (after first line)'
245 return HookFailure(msg)
246
Ryan Cuiec4d6332011-05-02 14:15:25 -0700247
248def _check_change_has_bug_field(project, commit):
249 """Check for a non-empty 'BUG=' field in the commit message."""
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700250 BUG_RE = r'\n\s*BUG\s*=[^\n]*\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700251
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700252 if not re.search(BUG_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700253 msg = 'Changelist description needs BUG field (after first line)'
254 return HookFailure(msg)
255
Ryan Cuiec4d6332011-05-02 14:15:25 -0700256
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700257def _check_change_has_proper_changeid(project, commit):
258 """Verify that Change-ID is present in last paragraph of commit message."""
259 desc = _get_commit_desc(commit)
260 loc = desc.rfind('\nChange-Id:')
261 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700262 return HookFailure('Change-Id must be in last paragraph of description.')
263
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700264
Ryan Cuiec4d6332011-05-02 14:15:25 -0700265def _check_license(project, commit):
266 """Verifies the license header."""
267 LICENSE_HEADER = (
268 r".*? Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
269 r"reserved\." "\n"
270 r".*? Use of this source code is governed by a BSD-style license that can "
271 "be\n"
272 r".*? found in the LICENSE file\."
273 "\n"
274 )
275
276 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
277 bad_files = []
278 files = _filter_files(_get_affected_files(commit),
279 COMMON_INCLUDED_PATHS,
280 COMMON_EXCLUDED_PATHS)
281
282 for f in files:
283 contents = open(f).read()
284 if len(contents) == 0: continue # Ignore empty files
285 if not license_re.search(contents):
286 bad_files.append(f)
287 if bad_files:
Ryan Cui1562fb82011-05-09 11:01:31 -0700288 return HookFailure('License must match:\n%s\n' % license_re.pattern +
289 'Found a bad license header in these files:',
290 bad_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700291
292
293# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700294
Ryan Cui1562fb82011-05-09 11:01:31 -0700295
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700296def _run_checkpatch(project, commit):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700297 """Runs checkpatch.pl on the given project"""
298 hooks_dir = _get_hooks_dir()
299 cmd = ['%s/checkpatch.pl' % hooks_dir, '-']
300 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700301 output = p.communicate(_get_diff(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700302 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700303 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700304
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700305
Dale Curtis2975c432011-05-03 17:25:20 -0700306def _run_json_check(project, commit):
307 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700308 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700309 try:
310 json.load(open(f))
311 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700312 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700313
314
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700315# Base
316
Ryan Cui1562fb82011-05-09 11:01:31 -0700317
Ryan Cuie37fe1a2011-05-03 19:00:10 -0700318COMMON_HOOKS = [_check_change_has_bug_field,
319 _check_change_has_test_field,
320 _check_change_has_proper_changeid,
Ryan Cuiec4d6332011-05-02 14:15:25 -0700321 _check_no_stray_whitespace,
Ryan Cui31e0c172011-05-04 21:00:45 -0700322 _check_no_long_lines,
323 _check_license,
324 _check_no_tabs]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700325
Ryan Cui1562fb82011-05-09 11:01:31 -0700326
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700327def _setup_project_hooks():
328 """Returns a dictionay of callbacks: dict[project] = [callback1, callback2]"""
329 return {
Doug Anderson830216f2011-05-02 10:08:37 -0700330 "chromiumos/third_party/kernel": [_run_checkpatch],
331 "chromiumos/third_party/kernel-next": [_run_checkpatch],
Dale Curtis2975c432011-05-03 17:25:20 -0700332 "chromeos/autotest-tools": [_run_json_check],
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700333 }
334
Ryan Cui1562fb82011-05-09 11:01:31 -0700335
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700336def _run_project_hooks(project, hooks):
Ryan Cui1562fb82011-05-09 11:01:31 -0700337 """For each project run its project specific hook from the hooks dictionary.
338
339 Args:
340 project: name of project to run hooks for.
341 hooks: a dictionary of hooks indexed by project name
342
343 Returns:
344 Boolean value of whether any errors were ecountered while running the hooks.
345 """
Ryan Cui72834d12011-05-05 14:51:33 -0700346 proj_dir = _run_command(['repo', 'forall', project, '-c', 'pwd']).strip()
Ryan Cuiec4d6332011-05-02 14:15:25 -0700347 pwd = os.getcwd()
348 # hooks assume they are run from the root of the project
349 os.chdir(proj_dir)
350
351 project_specific_hooks = []
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700352 if project in hooks:
Ryan Cuiec4d6332011-05-02 14:15:25 -0700353 project_specific_hooks = hooks[project]
354
Ryan Cuifa55df52011-05-06 11:16:55 -0700355 try:
356 commit_list = _get_commits()
Don Garrettdba548a2011-05-05 15:17:14 -0700357 except VerifyException as e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700358 PrintErrorForProject(project, HookFailure(str(e)))
359 os.chdir(pwd)
360 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700361
Ryan Cui1562fb82011-05-09 11:01:31 -0700362 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700363 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700364 error_list = []
365 for hook in COMMON_HOOKS + project_specific_hooks:
366 hook_error = hook(project, commit)
367 if hook_error:
368 error_list.append(hook_error)
369 error_found = True
370 if error_list:
371 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
372 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700373
Ryan Cuiec4d6332011-05-02 14:15:25 -0700374 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700375 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700376
Ryan Cui72834d12011-05-05 14:51:33 -0700377
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700378# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700379
Ryan Cui1562fb82011-05-09 11:01:31 -0700380
Anush Elangovan63afad72011-03-23 00:41:27 -0700381def main(project_list, **kwargs):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700382 hooks = _setup_project_hooks()
Don Garrettdba548a2011-05-05 15:17:14 -0700383
Ryan Cui1562fb82011-05-09 11:01:31 -0700384 found_error = False
385 for project in project_list:
386 if _run_project_hooks(project, hooks):
387 found_error = True
388
389 if (found_error):
390 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
391 '- To upload only current project, run \'repo upload .\'\n'
392 '- Errors may also be due to old upload hooks. Please run '
393 '\'repo sync chromiumos/repohooks\' to update.')
394 print >> sys.stderr, msg
Don Garrettdba548a2011-05-05 15:17:14 -0700395 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700396
Ryan Cui1562fb82011-05-09 11:01:31 -0700397
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700398if __name__ == '__main__':
399 main()