| 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 |