OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 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 """Enables directory-specific presubmit checks to run at upload and/or commit. | 6 """Enables directory-specific presubmit checks to run at upload and/or commit. |
7 """ | 7 """ |
8 | 8 |
9 __version__ = '1.1' | 9 __version__ = '1.1' |
10 | 10 |
11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow | 11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow |
12 # caching (between all different invocations of presubmit scripts for a given | 12 # caching (between all different invocations of presubmit scripts for a given |
13 # change). We should add it as our presubmit scripts start feeling slow. | 13 # change). We should add it as our presubmit scripts start feeling slow. |
14 | 14 |
15 import cPickle # Exposed through the API. | 15 import cPickle # Exposed through the API. |
16 import cStringIO # Exposed through the API. | 16 import cStringIO # Exposed through the API. |
17 import exceptions | 17 import exceptions |
18 import fnmatch | 18 import fnmatch |
19 import glob | 19 import glob |
20 import marshal # Exposed through the API. | 20 import marshal # Exposed through the API. |
21 import optparse | 21 import optparse |
22 import os # Somewhat exposed through the API. | 22 import os # Somewhat exposed through the API. |
23 import pickle # Exposed through the API. | 23 import pickle # Exposed through the API. |
24 import re # Exposed through the API. | 24 import re # Exposed through the API. |
25 import subprocess # Exposed through the API. | 25 import subprocess # Exposed through the API. |
26 import sys # Parts exposed through API. | 26 import sys # Parts exposed through API. |
27 import tempfile # Exposed through the API. | 27 import tempfile # Exposed through the API. |
28 import types | 28 import types |
29 import urllib2 # Exposed through the API. | 29 import urllib2 # Exposed through the API. |
30 import warnings | |
30 | 31 |
31 # Local imports. | 32 # Local imports. |
32 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but | 33 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but |
33 # for now it would only be a couple of functions so hardly worth it. | 34 # for now it would only be a couple of functions so hardly worth it. |
34 import gcl | 35 import gcl |
35 import gclient | 36 import gclient |
36 import presubmit_canned_checks | 37 import presubmit_canned_checks |
37 | 38 |
38 | 39 |
39 class NotImplementedException(Exception): | 40 class NotImplementedException(Exception): |
40 """We're leaving placeholders in a bunch of places to remind us of the | 41 """We're leaving placeholders in a bunch of places to remind us of the |
41 design of the API, but we have not implemented all of it yet. Implement as | 42 design of the API, but we have not implemented all of it yet. Implement as |
42 the need arises. | 43 the need arises. |
43 """ | 44 """ |
44 pass | 45 pass |
45 | 46 |
46 | 47 |
47 def normpath(path): | 48 def normpath(path): |
48 '''Version of os.path.normpath that also changes backward slashes to | 49 '''Version of os.path.normpath that also changes backward slashes to |
49 forward slashes when not running on Windows. | 50 forward slashes when not running on Windows. |
50 ''' | 51 ''' |
51 # This is safe to always do because the Windows version of os.path.normpath | 52 # This is safe to always do because the Windows version of os.path.normpath |
52 # will replace forward slashes with backward slashes. | 53 # will replace forward slashes with backward slashes. |
53 path = path.replace(os.sep, '/') | 54 path = path.replace(os.sep, '/') |
54 return os.path.normpath(path) | 55 return os.path.normpath(path) |
55 | 56 |
56 | 57 |
58 def deprecated(func): | |
59 """This is a decorator which can be used to mark functions as deprecated. | |
60 | |
61 It will result in a warning being emmitted when the function is used.""" | |
62 def newFunc(*args, **kwargs): | |
63 warnings.warn("Call to deprecated function %s." % func.__name__, | |
64 category=DeprecationWarning, | |
65 stacklevel=2) | |
66 return func(*args, **kwargs) | |
67 newFunc.__name__ = func.__name__ | |
68 newFunc.__doc__ = func.__doc__ | |
69 newFunc.__dict__.update(func.__dict__) | |
70 return newFunc | |
71 | |
72 | |
57 class OutputApi(object): | 73 class OutputApi(object): |
58 """This class (more like a module) gets passed to presubmit scripts so that | 74 """This class (more like a module) gets passed to presubmit scripts so that |
59 they can specify various types of results. | 75 they can specify various types of results. |
60 """ | 76 """ |
61 | 77 |
62 class PresubmitResult(object): | 78 class PresubmitResult(object): |
63 """Base class for result objects.""" | 79 """Base class for result objects.""" |
64 | 80 |
65 def __init__(self, message, items=None, long_text=''): | 81 def __init__(self, message, items=None, long_text=''): |
66 """ | 82 """ |
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
170 def PresubmitLocalPath(self): | 186 def PresubmitLocalPath(self): |
171 """Returns the local path of the presubmit script currently being run. | 187 """Returns the local path of the presubmit script currently being run. |
172 | 188 |
173 This is useful if you don't want to hard-code absolute paths in the | 189 This is useful if you don't want to hard-code absolute paths in the |
174 presubmit script. For example, It can be used to find another file | 190 presubmit script. For example, It can be used to find another file |
175 relative to the PRESUBMIT.py script, so the whole tree can be branched and | 191 relative to the PRESUBMIT.py script, so the whole tree can be branched and |
176 the presubmit script still works, without editing its content. | 192 the presubmit script still works, without editing its content. |
177 """ | 193 """ |
178 return self._current_presubmit_path | 194 return self._current_presubmit_path |
179 | 195 |
180 @staticmethod | 196 def DepotToLocalPath(self, depot_path): |
Jói Sigurðsson
2009/05/28 16:49:36
why make non-static when it doesn't use self?
| |
181 def DepotToLocalPath(depot_path): | |
182 """Translate a depot path to a local path (relative to client root). | 197 """Translate a depot path to a local path (relative to client root). |
183 | 198 |
184 Args: | 199 Args: |
185 Depot path as a string. | 200 Depot path as a string. |
186 | 201 |
187 Returns: | 202 Returns: |
188 The local path of the depot path under the user's current client, or None | 203 The local path of the depot path under the user's current client, or None |
189 if the file is not mapped. | 204 if the file is not mapped. |
190 | 205 |
191 Remember to check for the None case and show an appropriate error! | 206 Remember to check for the None case and show an appropriate error! |
192 """ | 207 """ |
193 local_path = gclient.CaptureSVNInfo(depot_path).get('Path') | 208 local_path = gclient.CaptureSVNInfo(depot_path).get('Path') |
194 if not local_path: | 209 if local_path: |
195 return None | |
196 else: | |
197 return local_path | 210 return local_path |
198 | 211 |
199 @staticmethod | 212 def LocalToDepotPath(self, local_path): |
200 def LocalToDepotPath(local_path): | |
201 """Translate a local path to a depot path. | 213 """Translate a local path to a depot path. |
202 | 214 |
203 Args: | 215 Args: |
204 Local path (relative to current directory, or absolute) as a string. | 216 Local path (relative to current directory, or absolute) as a string. |
205 | 217 |
206 Returns: | 218 Returns: |
207 The depot path (SVN URL) of the file if mapped, otherwise None. | 219 The depot path (SVN URL) of the file if mapped, otherwise None. |
208 """ | 220 """ |
209 depot_path = gclient.CaptureSVNInfo(local_path).get('URL') | 221 depot_path = gclient.CaptureSVNInfo(local_path).get('URL') |
210 if not depot_path: | 222 if depot_path: |
211 return None | |
212 else: | |
213 return depot_path | 223 return depot_path |
214 | 224 |
215 @staticmethod | 225 @staticmethod |
216 def FilterTextFiles(affected_files, include_deletes=True): | 226 def FilterTextFiles(affected_files, include_deletes=True): |
217 """Filters out all except text files and optionally also filters out | 227 """Filters out all except text files and optionally also filters out |
218 deleted files. | 228 deleted files. |
219 | 229 |
220 Args: | 230 Args: |
221 affected_files: List of AffectedFiles objects. | 231 affected_files: List of AffectedFiles objects. |
222 include_deletes: If false, deleted files will be filtered out. | 232 include_deletes: If false, deleted files will be filtered out. |
(...skipping 30 matching lines...) Expand all Loading... | |
253 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] | 263 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)] |
254 | 264 |
255 def AbsoluteLocalPaths(self, include_dirs=False): | 265 def AbsoluteLocalPaths(self, include_dirs=False): |
256 """Returns absolute local paths of input_api.AffectedFiles().""" | 266 """Returns absolute local paths of input_api.AffectedFiles().""" |
257 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] | 267 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)] |
258 | 268 |
259 def ServerPaths(self, include_dirs=False): | 269 def ServerPaths(self, include_dirs=False): |
260 """Returns server paths of input_api.AffectedFiles().""" | 270 """Returns server paths of input_api.AffectedFiles().""" |
261 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] | 271 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)] |
262 | 272 |
273 @deprecated | |
263 def AffectedTextFiles(self, include_deletes=True): | 274 def AffectedTextFiles(self, include_deletes=True): |
264 """Same as input_api.change.AffectedTextFiles() except only lists files | 275 """Same as input_api.change.AffectedTextFiles() except only lists files |
265 in the same directory as the current presubmit script, or subdirectories | 276 in the same directory as the current presubmit script, or subdirectories |
266 thereof. | 277 thereof. |
267 | 278 |
268 Warning: This function retrieves the svn property on each file so it can be | 279 Warning: This function retrieves the svn property on each file so it can be |
269 slow for large change lists. | 280 slow for large change lists. |
270 """ | 281 """ |
271 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), | 282 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), |
272 include_deletes) | 283 include_deletes) |
273 | 284 |
274 def RightHandSideLines(self): | 285 def RightHandSideLines(self): |
275 """An iterator over all text lines in "new" version of changed files. | 286 """An iterator over all text lines in "new" version of changed files. |
276 | 287 |
277 Only lists lines from new or modified text files in the change that are | 288 Only lists lines from new or modified text files in the change that are |
278 contained by the directory of the currently executing presubmit script. | 289 contained by the directory of the currently executing presubmit script. |
279 | 290 |
280 This is useful for doing line-by-line regex checks, like checking for | 291 This is useful for doing line-by-line regex checks, like checking for |
281 trailing whitespace. | 292 trailing whitespace. |
282 | 293 |
283 Yields: | 294 Yields: |
284 a 3 tuple: | 295 a 3 tuple: |
285 the AffectedFile instance of the current file; | 296 the AffectedFile instance of the current file; |
286 integer line number (1-based); and | 297 integer line number (1-based); and |
287 the contents of the line as a string. | 298 the contents of the line as a string. |
288 """ | 299 """ |
289 return InputApi._RightHandSideLinesImpl( | 300 return InputApi._RightHandSideLinesImpl( |
290 self.AffectedTextFiles(include_deletes=False)) | 301 filter(lambda x: x.IsTextFile(), |
302 self.AffectedFiles(include_deletes=False))) | |
291 | 303 |
292 @staticmethod | 304 @staticmethod |
293 def _RightHandSideLinesImpl(affected_files): | 305 def _RightHandSideLinesImpl(affected_files): |
294 """Implements RightHandSideLines for InputApi and GclChange.""" | 306 """Implements RightHandSideLines for InputApi and GclChange.""" |
295 for af in affected_files: | 307 for af in affected_files: |
296 lines = af.NewContents() | 308 lines = af.NewContents() |
297 line_number = 0 | 309 line_number = 0 |
298 for line in lines: | 310 for line in lines: |
299 line_number += 1 | 311 line_number += 1 |
300 yield (af, line_number, line) | 312 yield (af, line_number, line) |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
341 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but | 353 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but |
342 # different for other SCM. | 354 # different for other SCM. |
343 return self.action | 355 return self.action |
344 | 356 |
345 def Property(self, property_name): | 357 def Property(self, property_name): |
346 """Returns the specified SCM property of this file, or None if no such | 358 """Returns the specified SCM property of this file, or None if no such |
347 property. | 359 property. |
348 """ | 360 """ |
349 return self.properties.get(property_name, None) | 361 return self.properties.get(property_name, None) |
350 | 362 |
363 def IsTextFile(self): | |
364 """Returns True if the file is a text file and not a binary file.""" | |
365 raise NotImplementedError() # Implement when needed | |
366 | |
351 def NewContents(self): | 367 def NewContents(self): |
352 """Returns an iterator over the lines in the new version of file. | 368 """Returns an iterator over the lines in the new version of file. |
353 | 369 |
354 The new version is the file in the user's workspace, i.e. the "right hand | 370 The new version is the file in the user's workspace, i.e. the "right hand |
355 side". | 371 side". |
356 | 372 |
357 Contents will be empty if the file is a directory or does not exist. | 373 Contents will be empty if the file is a directory or does not exist. |
358 """ | 374 """ |
359 if self.IsDirectory(): | 375 if self.IsDirectory(): |
360 return [] | 376 return [] |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
398 self.is_directory = gclient.CaptureSVNInfo( | 414 self.is_directory = gclient.CaptureSVNInfo( |
399 path).get('Node Kind') in ('dir', 'directory') | 415 path).get('Node Kind') in ('dir', 'directory') |
400 return self.is_directory | 416 return self.is_directory |
401 | 417 |
402 def Property(self, property_name): | 418 def Property(self, property_name): |
403 if not property_name in self.properties: | 419 if not property_name in self.properties: |
404 self.properties[property_name] = gcl.GetSVNFileProperty( | 420 self.properties[property_name] = gcl.GetSVNFileProperty( |
405 self.AbsoluteLocalPath(), property_name) | 421 self.AbsoluteLocalPath(), property_name) |
406 return self.properties[property_name] | 422 return self.properties[property_name] |
407 | 423 |
424 def IsTextFile(self): | |
425 if self.Action() == 'D': | |
426 return False | |
427 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), | |
428 'svn:mime-type') | |
429 if not mime_type or mime_type.startswith('text/'): | |
430 return True | |
431 return False | |
432 | |
408 | 433 |
409 class GclChange(object): | 434 class GclChange(object): |
410 """Describe a change. | 435 """Describe a change. |
411 | 436 |
412 Used directly by the presubmit scripts to query the current change being | 437 Used directly by the presubmit scripts to query the current change being |
413 tested. | 438 tested. |
414 | 439 |
415 Instance members: | 440 Instance members: |
416 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 441 tags: Dictionnary of KEY=VALUE pairs found in the change description. |
417 self.KEY: equivalent to tags['KEY'] | 442 self.KEY: equivalent to tags['KEY'] |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
489 if include_dirs: | 514 if include_dirs: |
490 affected = self._affected_files | 515 affected = self._affected_files |
491 else: | 516 else: |
492 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) | 517 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) |
493 | 518 |
494 if include_deletes: | 519 if include_deletes: |
495 return affected | 520 return affected |
496 else: | 521 else: |
497 return filter(lambda x: x.Action() != 'D', affected) | 522 return filter(lambda x: x.Action() != 'D', affected) |
498 | 523 |
524 @deprecated | |
499 def AffectedTextFiles(self, include_deletes=True): | 525 def AffectedTextFiles(self, include_deletes=True): |
500 """Return a list of the text files in a change. | 526 """Return a list of the text files in a change. |
501 | 527 |
502 It's common to want to iterate over only the text files. | 528 It's common to want to iterate over only the text files. |
503 | 529 |
504 Args: | 530 Args: |
505 include_deletes: Controls whether to return files with "delete" actions, | 531 include_deletes: Controls whether to return files with "delete" actions, |
506 which commonly aren't relevant to presubmit scripts. | 532 which commonly aren't relevant to presubmit scripts. |
507 """ | 533 """ |
508 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), | 534 return InputApi.FilterTextFiles(self.AffectedFiles(include_dirs=False), |
(...skipping 19 matching lines...) Expand all Loading... | |
528 This is useful for doing line-by-line regex checks, like checking for | 554 This is useful for doing line-by-line regex checks, like checking for |
529 trailing whitespace. | 555 trailing whitespace. |
530 | 556 |
531 Yields: | 557 Yields: |
532 a 3 tuple: | 558 a 3 tuple: |
533 the AffectedFile instance of the current file; | 559 the AffectedFile instance of the current file; |
534 integer line number (1-based); and | 560 integer line number (1-based); and |
535 the contents of the line as a string. | 561 the contents of the line as a string. |
536 """ | 562 """ |
537 return InputApi._RightHandSideLinesImpl( | 563 return InputApi._RightHandSideLinesImpl( |
538 self.AffectedTextFiles(include_deletes=False)) | 564 filter(lambda x: x.IsTextFile(), |
565 self.AffectedFiles(include_deletes=False))) | |
539 | 566 |
540 | 567 |
541 def ListRelevantPresubmitFiles(files): | 568 def ListRelevantPresubmitFiles(files): |
542 """Finds all presubmit files that apply to a given set of source files. | 569 """Finds all presubmit files that apply to a given set of source files. |
543 | 570 |
544 Args: | 571 Args: |
545 files: An iterable container containing file paths. | 572 files: An iterable container containing file paths. |
546 | 573 |
547 Return: | 574 Return: |
548 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py'] | 575 ['foo/blat/PRESUBMIT.py', 'mat/gat/PRESUBMIT.py'] |
(...skipping 18 matching lines...) Expand all Loading... | |
567 return presubmit_files | 594 return presubmit_files |
568 | 595 |
569 | 596 |
570 class PresubmitExecuter(object): | 597 class PresubmitExecuter(object): |
571 def __init__(self, change_info, committing): | 598 def __init__(self, change_info, committing): |
572 """ | 599 """ |
573 Args: | 600 Args: |
574 change_info: The ChangeInfo object for the change. | 601 change_info: The ChangeInfo object for the change. |
575 committing: True if 'gcl commit' is running, False if 'gcl upload' is. | 602 committing: True if 'gcl commit' is running, False if 'gcl upload' is. |
576 """ | 603 """ |
604 # TODO(maruel): Determine the SCM. | |
577 self.change = GclChange(change_info, gcl.GetRepositoryRoot()) | 605 self.change = GclChange(change_info, gcl.GetRepositoryRoot()) |
578 self.committing = committing | 606 self.committing = committing |
579 | 607 |
580 def ExecPresubmitScript(self, script_text, presubmit_path): | 608 def ExecPresubmitScript(self, script_text, presubmit_path): |
581 """Executes a single presubmit script. | 609 """Executes a single presubmit script. |
582 | 610 |
583 Args: | 611 Args: |
584 script_text: The text of the presubmit script. | 612 script_text: The text of the presubmit script. |
585 presubmit_path: The path to the presubmit file (this will be reported via | 613 presubmit_path: The path to the presubmit file (this will be reported via |
586 input_api.PresubmitLocalPath()). | 614 input_api.PresubmitLocalPath()). |
(...skipping 140 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
727 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files), | 755 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files), |
728 options.commit, | 756 options.commit, |
729 options.verbose, | 757 options.verbose, |
730 sys.stdout, | 758 sys.stdout, |
731 sys.stdin, | 759 sys.stdin, |
732 default_presubmit=None) | 760 default_presubmit=None) |
733 | 761 |
734 | 762 |
735 if __name__ == '__main__': | 763 if __name__ == '__main__': |
736 sys.exit(Main(sys.argv)) | 764 sys.exit(Main(sys.argv)) |
OLD | NEW |