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 |
(...skipping 18 matching lines...) Expand all Loading... |
29 import urllib2 # Exposed through the API. | 29 import urllib2 # Exposed through the API. |
30 | 30 |
31 # Local imports. | 31 # Local imports. |
32 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but | 32 # 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. | 33 # for now it would only be a couple of functions so hardly worth it. |
34 import gcl | 34 import gcl |
35 import gclient | 35 import gclient |
36 import presubmit_canned_checks | 36 import presubmit_canned_checks |
37 | 37 |
38 | 38 |
39 # Matches key/value (or "tag") lines in changelist descriptions. | |
40 _tag_line_re = re.compile( | |
41 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') | |
42 | |
43 | |
44 class NotImplementedException(Exception): | 39 class NotImplementedException(Exception): |
45 """We're leaving placeholders in a bunch of places to remind us of the | 40 """We're leaving placeholders in a bunch of places to remind us of the |
46 design of the API, but we have not implemented all of it yet. Implement as | 41 design of the API, but we have not implemented all of it yet. Implement as |
47 the need arises. | 42 the need arises. |
48 """ | 43 """ |
49 pass | 44 pass |
50 | 45 |
51 | 46 |
52 def normpath(path): | 47 def normpath(path): |
53 '''Version of os.path.normpath that also changes backward slashes to | 48 '''Version of os.path.normpath that also changes backward slashes to |
54 forward slashes when not running on Windows. | 49 forward slashes when not running on Windows. |
55 ''' | 50 ''' |
56 # This is safe to always do because the Windows version of os.path.normpath | 51 # This is safe to always do because the Windows version of os.path.normpath |
57 # will replace forward slashes with backward slashes. | 52 # will replace forward slashes with backward slashes. |
58 path = path.replace(os.sep, '/') | 53 path = path.replace(os.sep, '/') |
59 return os.path.normpath(path) | 54 return os.path.normpath(path) |
60 | 55 |
61 | 56 |
62 | |
63 class OutputApi(object): | 57 class OutputApi(object): |
64 """This class (more like a module) gets passed to presubmit scripts so that | 58 """This class (more like a module) gets passed to presubmit scripts so that |
65 they can specify various types of results. | 59 they can specify various types of results. |
66 """ | 60 """ |
67 | 61 |
68 class PresubmitResult(object): | 62 class PresubmitResult(object): |
69 """Base class for result objects.""" | 63 """Base class for result objects.""" |
70 | 64 |
71 def __init__(self, message, items=None, long_text=''): | 65 def __init__(self, message, items=None, long_text=''): |
72 """ | 66 """ |
(...skipping 333 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
406 return self.is_directory | 400 return self.is_directory |
407 | 401 |
408 def Property(self, property_name): | 402 def Property(self, property_name): |
409 if not property_name in self.properties: | 403 if not property_name in self.properties: |
410 self.properties[property_name] = gcl.GetSVNFileProperty( | 404 self.properties[property_name] = gcl.GetSVNFileProperty( |
411 self.AbsoluteLocalPath(), property_name) | 405 self.AbsoluteLocalPath(), property_name) |
412 return self.properties[property_name] | 406 return self.properties[property_name] |
413 | 407 |
414 | 408 |
415 class GclChange(object): | 409 class GclChange(object): |
416 """A gcl change. See gcl.ChangeInfo for more info.""" | 410 """Describe a change. |
| 411 |
| 412 Used directly by the presubmit scripts to query the current change being |
| 413 tested. |
| 414 |
| 415 Instance members: |
| 416 tags: Dictionnary of KEY=VALUE pairs found in the change description. |
| 417 self.KEY: equivalent to tags['KEY'] |
| 418 """ |
| 419 |
| 420 # Matches key/value (or "tag") lines in changelist descriptions. |
| 421 _tag_line_re = re.compile( |
| 422 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') |
417 | 423 |
418 def __init__(self, change_info, repository_root=''): | 424 def __init__(self, change_info, repository_root=''): |
419 self.name = change_info.name | 425 # Do not keep a reference to the original change_info. |
420 self.full_description = change_info.description | 426 self._name = change_info.name |
421 self.repository_root = repository_root | 427 self._full_description = change_info.description |
| 428 self._repository_root = repository_root |
422 | 429 |
423 # From the description text, build up a dictionary of key/value pairs | 430 # From the description text, build up a dictionary of key/value pairs |
424 # plus the description minus all key/value or "tag" lines. | 431 # plus the description minus all key/value or "tag" lines. |
425 self.description_without_tags = [] | 432 self._description_without_tags = [] |
426 self.tags = {} | 433 self.tags = {} |
427 for line in change_info.description.splitlines(): | 434 for line in change_info.description.splitlines(): |
428 m = _tag_line_re.match(line) | 435 m = self._tag_line_re.match(line) |
429 if m: | 436 if m: |
430 self.tags[m.group('key')] = m.group('value') | 437 self.tags[m.group('key')] = m.group('value') |
431 else: | 438 else: |
432 self.description_without_tags.append(line) | 439 self._description_without_tags.append(line) |
433 | 440 |
434 # Change back to text and remove whitespace at end. | 441 # Change back to text and remove whitespace at end. |
435 self.description_without_tags = '\n'.join(self.description_without_tags) | 442 self._description_without_tags = '\n'.join(self._description_without_tags) |
436 self.description_without_tags = self.description_without_tags.rstrip() | 443 self._description_without_tags = self._description_without_tags.rstrip() |
437 | 444 |
438 self.affected_files = [ | 445 self._affected_files = [ |
439 SvnAffectedFile(info[1], info[0].strip(), repository_root) | 446 SvnAffectedFile(info[1], info[0].strip(), repository_root) |
440 for info in change_info.files | 447 for info in change_info.files |
441 ] | 448 ] |
442 | 449 |
443 def Change(self): | 450 def Change(self): |
444 """Returns the change name.""" | 451 """Returns the change name.""" |
445 return self.name | 452 return self._name |
446 | 453 |
447 def DescriptionText(self): | 454 def DescriptionText(self): |
448 """Returns the user-entered changelist description, minus tags. | 455 """Returns the user-entered changelist description, minus tags. |
449 | 456 |
450 Any line in the user-provided description starting with e.g. "FOO=" | 457 Any line in the user-provided description starting with e.g. "FOO=" |
451 (whitespace permitted before and around) is considered a tag line. Such | 458 (whitespace permitted before and around) is considered a tag line. Such |
452 lines are stripped out of the description this function returns. | 459 lines are stripped out of the description this function returns. |
453 """ | 460 """ |
454 return self.description_without_tags | 461 return self._description_without_tags |
455 | 462 |
456 def FullDescriptionText(self): | 463 def FullDescriptionText(self): |
457 """Returns the complete changelist description including tags.""" | 464 """Returns the complete changelist description including tags.""" |
458 return self.full_description | 465 return self._full_description |
459 | 466 |
460 def RepositoryRoot(self): | 467 def RepositoryRoot(self): |
461 """Returns the repository root for this change, as an absolute path.""" | 468 """Returns the repository root for this change, as an absolute path.""" |
462 return self.repository_root | 469 return self._repository_root |
463 | 470 |
464 def __getattr__(self, attr): | 471 def __getattr__(self, attr): |
465 """Return keys directly as attributes on the object. | 472 """Return keys directly as attributes on the object. |
466 | 473 |
467 You may use a friendly name (from SPECIAL_KEYS) or the actual name of | 474 You may use a friendly name (from SPECIAL_KEYS) or the actual name of |
468 the key. | 475 the key. |
469 """ | 476 """ |
470 return self.tags.get(attr) | 477 return self.tags.get(attr) |
471 | 478 |
472 def AffectedFiles(self, include_dirs=False, include_deletes=True): | 479 def AffectedFiles(self, include_dirs=False, include_deletes=True): |
473 """Returns a list of AffectedFile instances for all files in the change. | 480 """Returns a list of AffectedFile instances for all files in the change. |
474 | 481 |
475 Args: | 482 Args: |
476 include_deletes: If false, deleted files will be filtered out. | 483 include_deletes: If false, deleted files will be filtered out. |
477 include_dirs: True to include directories in the list | 484 include_dirs: True to include directories in the list |
478 | 485 |
479 Returns: | 486 Returns: |
480 [AffectedFile(path, action), AffectedFile(path, action)] | 487 [AffectedFile(path, action), AffectedFile(path, action)] |
481 """ | 488 """ |
482 if include_dirs: | 489 if include_dirs: |
483 affected = self.affected_files | 490 affected = self._affected_files |
484 else: | 491 else: |
485 affected = filter(lambda x: not x.IsDirectory(), self.affected_files) | 492 affected = filter(lambda x: not x.IsDirectory(), self._affected_files) |
486 | 493 |
487 if include_deletes: | 494 if include_deletes: |
488 return affected | 495 return affected |
489 else: | 496 else: |
490 return filter(lambda x: x.Action() != 'D', affected) | 497 return filter(lambda x: x.Action() != 'D', affected) |
491 | 498 |
492 def AffectedTextFiles(self, include_deletes=True): | 499 def AffectedTextFiles(self, include_deletes=True): |
493 """Return a list of the text files in a change. | 500 """Return a list of the text files in a change. |
494 | 501 |
495 It's common to want to iterate over only the text files. | 502 It's common to want to iterate over only the text files. |
(...skipping 224 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
720 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files), | 727 return not DoPresubmitChecks(gcl.ChangeInfo(name='temp', files=files), |
721 options.commit, | 728 options.commit, |
722 options.verbose, | 729 options.verbose, |
723 sys.stdout, | 730 sys.stdout, |
724 sys.stdin, | 731 sys.stdin, |
725 default_presubmit=None) | 732 default_presubmit=None) |
726 | 733 |
727 | 734 |
728 if __name__ == '__main__': | 735 if __name__ == '__main__': |
729 sys.exit(Main(sys.argv)) | 736 sys.exit(Main(sys.argv)) |
OLD | NEW |