Chromium Code Reviews| 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 |