| OLD | NEW |
| 1 # coding=utf8 | 1 # coding=utf8 |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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 """Manages a project checkout. | 5 """Manages a project checkout. |
| 6 | 6 |
| 7 Includes support for svn, git-svn and git. | 7 Includes support for svn, git-svn and git. |
| 8 """ | 8 """ |
| 9 | 9 |
| 10 import ConfigParser | 10 import ConfigParser |
| 11 import fnmatch | 11 import fnmatch |
| 12 import logging | 12 import logging |
| 13 import os | 13 import os |
| 14 import re | 14 import re |
| 15 import shutil | 15 import shutil |
| 16 import subprocess | 16 import subprocess |
| 17 import sys | 17 import sys |
| 18 import tempfile | 18 import tempfile |
| 19 | 19 |
| 20 import patch | 20 import patch |
| 21 import scm | 21 import scm |
| 22 import subprocess2 | 22 import subprocess2 |
| 23 | 23 |
| 24 | 24 |
| 25 # Default timeout of 15 minutes. | |
| 26 GLOBAL_TIMEOUT = 15*60 | |
| 27 # Use a larger timeout for checkout since it can be a genuinely slower | |
| 28 # operation. | |
| 29 FETCH_TIMEOUT = 30*60 | |
| 30 | |
| 31 | |
| 32 def get_code_review_setting(path, key, | 25 def get_code_review_setting(path, key, |
| 33 codereview_settings_file='codereview.settings'): | 26 codereview_settings_file='codereview.settings'): |
| 34 """Parses codereview.settings and return the value for the key if present. | 27 """Parses codereview.settings and return the value for the key if present. |
| 35 | 28 |
| 36 Don't cache the values in case the file is changed.""" | 29 Don't cache the values in case the file is changed.""" |
| 37 # TODO(maruel): Do not duplicate code. | 30 # TODO(maruel): Do not duplicate code. |
| 38 settings = {} | 31 settings = {} |
| 39 try: | 32 try: |
| 40 settings_file = open(os.path.join(path, codereview_settings_file), 'r') | 33 settings_file = open(os.path.join(path, codereview_settings_file), 'r') |
| 41 try: | 34 try: |
| (...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 197 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename)) | 190 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename)) |
| 198 if p.diff_hunks: | 191 if p.diff_hunks: |
| 199 cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel] | 192 cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel] |
| 200 if verbose: | 193 if verbose: |
| 201 cmd.append('--verbose') | 194 cmd.append('--verbose') |
| 202 stdout.append( | 195 stdout.append( |
| 203 subprocess2.check_output( | 196 subprocess2.check_output( |
| 204 cmd, | 197 cmd, |
| 205 stdin=p.get(False), | 198 stdin=p.get(False), |
| 206 stderr=subprocess2.STDOUT, | 199 stderr=subprocess2.STDOUT, |
| 207 cwd=self.project_path, | 200 cwd=self.project_path)) |
| 208 timeout=GLOBAL_TIMEOUT)) | |
| 209 elif p.is_new and not os.path.exists(filepath): | 201 elif p.is_new and not os.path.exists(filepath): |
| 210 # There is only a header. Just create the file. | 202 # There is only a header. Just create the file. |
| 211 open(filepath, 'w').close() | 203 open(filepath, 'w').close() |
| 212 stdout.append('Created an empty file.') | 204 stdout.append('Created an empty file.') |
| 213 for post in post_processors: | 205 for post in post_processors: |
| 214 post(self, p) | 206 post(self, p) |
| 215 if verbose: | 207 if verbose: |
| 216 print p.filename | 208 print p.filename |
| 217 print align_stdout(stdout) | 209 print align_stdout(stdout) |
| 218 except OSError, e: | 210 except OSError, e: |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 277 if self.commit_user: | 269 if self.commit_user: |
| 278 args.extend(['--username', self.commit_user]) | 270 args.extend(['--username', self.commit_user]) |
| 279 if self.commit_pwd: | 271 if self.commit_pwd: |
| 280 args.extend(['--password', self.commit_pwd]) | 272 args.extend(['--password', self.commit_pwd]) |
| 281 return args | 273 return args |
| 282 | 274 |
| 283 def _check_call_svn(self, args, **kwargs): | 275 def _check_call_svn(self, args, **kwargs): |
| 284 """Runs svn and throws an exception if the command failed.""" | 276 """Runs svn and throws an exception if the command failed.""" |
| 285 kwargs.setdefault('cwd', self.project_path) | 277 kwargs.setdefault('cwd', self.project_path) |
| 286 kwargs.setdefault('stdout', self.VOID) | 278 kwargs.setdefault('stdout', self.VOID) |
| 287 kwargs.setdefault('timeout', GLOBAL_TIMEOUT) | |
| 288 return subprocess2.check_call_out( | 279 return subprocess2.check_call_out( |
| 289 self._add_svn_flags(args, False), | 280 self._add_svn_flags(args, False), **kwargs) |
| 290 **kwargs) | |
| 291 | 281 |
| 292 def _check_output_svn(self, args, credentials=True, **kwargs): | 282 def _check_output_svn(self, args, credentials=True, **kwargs): |
| 293 """Runs svn and throws an exception if the command failed. | 283 """Runs svn and throws an exception if the command failed. |
| 294 | 284 |
| 295 Returns the output. | 285 Returns the output. |
| 296 """ | 286 """ |
| 297 kwargs.setdefault('cwd', self.project_path) | 287 kwargs.setdefault('cwd', self.project_path) |
| 298 return subprocess2.check_output( | 288 return subprocess2.check_output( |
| 299 self._add_svn_flags(args, True, credentials), | 289 self._add_svn_flags(args, True, credentials), |
| 300 stderr=subprocess2.STDOUT, | 290 stderr=subprocess2.STDOUT, |
| 301 timeout=GLOBAL_TIMEOUT, | |
| 302 **kwargs) | 291 **kwargs) |
| 303 | 292 |
| 304 @staticmethod | 293 @staticmethod |
| 305 def _parse_svn_info(output, key): | 294 def _parse_svn_info(output, key): |
| 306 """Returns value for key from svn info output. | 295 """Returns value for key from svn info output. |
| 307 | 296 |
| 308 Case insensitive. | 297 Case insensitive. |
| 309 """ | 298 """ |
| 310 values = {} | 299 values = {} |
| 311 key = key.lower() | 300 key = key.lower() |
| (...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 391 if p.diff_hunks: | 380 if p.diff_hunks: |
| 392 cmd = [ | 381 cmd = [ |
| 393 'patch', | 382 'patch', |
| 394 '-p%s' % p.patchlevel, | 383 '-p%s' % p.patchlevel, |
| 395 '--forward', | 384 '--forward', |
| 396 '--force', | 385 '--force', |
| 397 '--no-backup-if-mismatch', | 386 '--no-backup-if-mismatch', |
| 398 ] | 387 ] |
| 399 stdout.append( | 388 stdout.append( |
| 400 subprocess2.check_output( | 389 subprocess2.check_output( |
| 401 cmd, | 390 cmd, stdin=p.get(False), cwd=self.project_path)) |
| 402 stdin=p.get(False), | |
| 403 cwd=self.project_path, | |
| 404 timeout=GLOBAL_TIMEOUT)) | |
| 405 elif p.is_new and not os.path.exists(filepath): | 391 elif p.is_new and not os.path.exists(filepath): |
| 406 # There is only a header. Just create the file if it doesn't | 392 # There is only a header. Just create the file if it doesn't |
| 407 # exist. | 393 # exist. |
| 408 open(filepath, 'w').close() | 394 open(filepath, 'w').close() |
| 409 stdout.append('Created an empty file.') | 395 stdout.append('Created an empty file.') |
| 410 if p.is_new and not p.source_filename: | 396 if p.is_new and not p.source_filename: |
| 411 # Do not run it if p.source_filename is defined, since svn copy was | 397 # Do not run it if p.source_filename is defined, since svn copy was |
| 412 # using above. | 398 # using above. |
| 413 stdout.append( | 399 stdout.append( |
| 414 self._check_output_svn( | 400 self._check_output_svn( |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 490 """Reverts local modifications or checks out if the directory is not | 476 """Reverts local modifications or checks out if the directory is not |
| 491 present. Use depot_tools's functionality to do this. | 477 present. Use depot_tools's functionality to do this. |
| 492 """ | 478 """ |
| 493 flags = ['--ignore-externals'] | 479 flags = ['--ignore-externals'] |
| 494 if revision: | 480 if revision: |
| 495 flags.extend(['--revision', str(revision)]) | 481 flags.extend(['--revision', str(revision)]) |
| 496 if not os.path.isdir(self.project_path): | 482 if not os.path.isdir(self.project_path): |
| 497 logging.info( | 483 logging.info( |
| 498 'Directory %s is not present, checking it out.' % self.project_path) | 484 'Directory %s is not present, checking it out.' % self.project_path) |
| 499 self._check_call_svn( | 485 self._check_call_svn( |
| 500 ['checkout', self.svn_url, self.project_path] + flags, | 486 ['checkout', self.svn_url, self.project_path] + flags, cwd=None) |
| 501 cwd=None, | |
| 502 timeout=FETCH_TIMEOUT) | |
| 503 else: | 487 else: |
| 504 # TODO(maruel): This command will shell out without a timeout. | |
| 505 scm.SVN.Revert(self.project_path, no_ignore=True) | 488 scm.SVN.Revert(self.project_path, no_ignore=True) |
| 506 # Revive files that were deleted in scm.SVN.Revert(). | 489 # Revive files that were deleted in scm.SVN.Revert(). |
| 507 self._check_call_svn(['update', '--force'] + flags, timeout=FETCH_TIMEOUT) | 490 self._check_call_svn(['update', '--force'] + flags) |
| 508 return self._get_revision() | 491 return self._get_revision() |
| 509 | 492 |
| 510 def _get_revision(self): | 493 def _get_revision(self): |
| 511 out = self._check_output_svn(['info', '.']) | 494 out = self._check_output_svn(['info', '.']) |
| 512 revision = int(self._parse_svn_info(out, 'revision')) | 495 revision = int(self._parse_svn_info(out, 'revision')) |
| 513 if revision != self._last_seen_revision: | 496 if revision != self._last_seen_revision: |
| 514 logging.info('Updated to revision %d' % revision) | 497 logging.info('Updated to revision %d' % revision) |
| 515 self._last_seen_revision = revision | 498 self._last_seen_revision = revision |
| 516 return revision | 499 return revision |
| 517 | 500 |
| (...skipping 29 matching lines...) Expand all Loading... |
| 547 | 530 |
| 548 Checks it out if not present and deletes the working branch. | 531 Checks it out if not present and deletes the working branch. |
| 549 """ | 532 """ |
| 550 assert self.remote_branch | 533 assert self.remote_branch |
| 551 assert os.path.isdir(self.project_path) | 534 assert os.path.isdir(self.project_path) |
| 552 self._check_call_git(['reset', '--hard', '--quiet']) | 535 self._check_call_git(['reset', '--hard', '--quiet']) |
| 553 if revision: | 536 if revision: |
| 554 try: | 537 try: |
| 555 revision = self._check_output_git(['rev-parse', revision]) | 538 revision = self._check_output_git(['rev-parse', revision]) |
| 556 except subprocess.CalledProcessError: | 539 except subprocess.CalledProcessError: |
| 557 self._fetch_remote() | 540 self._check_call_git( |
| 541 ['fetch', self.remote, self.remote_branch, '--quiet']) |
| 558 revision = self._check_output_git(['rev-parse', revision]) | 542 revision = self._check_output_git(['rev-parse', revision]) |
| 559 self._check_call_git(['checkout', '--force', '--quiet', revision]) | 543 self._check_call_git(['checkout', '--force', '--quiet', revision]) |
| 560 else: | 544 else: |
| 561 branches, active = self._branches() | 545 branches, active = self._branches() |
| 562 if active != 'master': | 546 if active != 'master': |
| 563 self._check_call_git(['checkout', '--force', '--quiet', 'master']) | 547 self._check_call_git(['checkout', '--force', '--quiet', 'master']) |
| 564 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) | 548 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) |
| 565 if self.working_branch in branches: | 549 if self.working_branch in branches: |
| 566 self._call_git(['branch', '-D', self.working_branch]) | 550 self._call_git(['branch', '-D', self.working_branch]) |
| 567 | 551 |
| (...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 658 | 642 |
| 659 Subclass needs to dcommit or push. | 643 Subclass needs to dcommit or push. |
| 660 """ | 644 """ |
| 661 assert isinstance(commit_message, unicode) | 645 assert isinstance(commit_message, unicode) |
| 662 self._check_call_git(['commit', '--amend', '-m', commit_message]) | 646 self._check_call_git(['commit', '--amend', '-m', commit_message]) |
| 663 return self._check_output_git(['rev-parse', 'HEAD']).strip() | 647 return self._check_output_git(['rev-parse', 'HEAD']).strip() |
| 664 | 648 |
| 665 def _check_call_git(self, args, **kwargs): | 649 def _check_call_git(self, args, **kwargs): |
| 666 kwargs.setdefault('cwd', self.project_path) | 650 kwargs.setdefault('cwd', self.project_path) |
| 667 kwargs.setdefault('stdout', self.VOID) | 651 kwargs.setdefault('stdout', self.VOID) |
| 668 kwargs.setdefault('timeout', GLOBAL_TIMEOUT) | 652 return subprocess2.check_call_out(['git'] + args, **kwargs) |
| 669 return subprocess2.check_call_out( | |
| 670 ['git'] + args, **kwargs) | |
| 671 | 653 |
| 672 def _call_git(self, args, **kwargs): | 654 def _call_git(self, args, **kwargs): |
| 673 """Like check_call but doesn't throw on failure.""" | 655 """Like check_call but doesn't throw on failure.""" |
| 674 kwargs.setdefault('cwd', self.project_path) | 656 kwargs.setdefault('cwd', self.project_path) |
| 675 kwargs.setdefault('stdout', self.VOID) | 657 kwargs.setdefault('stdout', self.VOID) |
| 676 return subprocess2.call(['git'] + args, timeout=GLOBAL_TIMEOUT, **kwargs) | 658 return subprocess2.call(['git'] + args, **kwargs) |
| 677 | 659 |
| 678 def _check_output_git(self, args, **kwargs): | 660 def _check_output_git(self, args, **kwargs): |
| 679 kwargs.setdefault('cwd', self.project_path) | 661 kwargs.setdefault('cwd', self.project_path) |
| 680 return subprocess2.check_output( | 662 return subprocess2.check_output( |
| 681 ['git'] + args, | 663 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs) |
| 682 stderr=subprocess2.STDOUT, | |
| 683 timeout=GLOBAL_TIMEOUT, | |
| 684 **kwargs) | |
| 685 | 664 |
| 686 def _branches(self): | 665 def _branches(self): |
| 687 """Returns the list of branches and the active one.""" | 666 """Returns the list of branches and the active one.""" |
| 688 out = self._check_output_git(['branch']).splitlines(False) | 667 out = self._check_output_git(['branch']).splitlines(False) |
| 689 branches = [l[2:] for l in out] | 668 branches = [l[2:] for l in out] |
| 690 active = None | 669 active = None |
| 691 for l in out: | 670 for l in out: |
| 692 if l.startswith('*'): | 671 if l.startswith('*'): |
| 693 active = l[2:] | 672 active = l[2:] |
| 694 break | 673 break |
| (...skipping 14 matching lines...) Expand all Loading... |
| 709 | 688 |
| 710 def _fetch_remote(self): | 689 def _fetch_remote(self): |
| 711 """Fetches the remote without rebasing.""" | 690 """Fetches the remote without rebasing.""" |
| 712 raise NotImplementedError() | 691 raise NotImplementedError() |
| 713 | 692 |
| 714 | 693 |
| 715 class GitCheckout(GitCheckoutBase): | 694 class GitCheckout(GitCheckoutBase): |
| 716 """Git checkout implementation.""" | 695 """Git checkout implementation.""" |
| 717 def _fetch_remote(self): | 696 def _fetch_remote(self): |
| 718 # git fetch is always verbose even with -q -q so redirect its output. | 697 # git fetch is always verbose even with -q -q so redirect its output. |
| 719 self._check_call_git( | 698 self._check_output_git(['fetch', self.remote, self.remote_branch]) |
| 720 ['fetch', self.remote, self.remote_branch, '--quiet'], | |
| 721 timeout=FETCH_TIMEOUT) | |
| 722 | 699 |
| 723 | 700 |
| 724 class ReadOnlyCheckout(object): | 701 class ReadOnlyCheckout(object): |
| 725 """Converts a checkout into a read-only one.""" | 702 """Converts a checkout into a read-only one.""" |
| 726 def __init__(self, checkout, post_processors=None): | 703 def __init__(self, checkout, post_processors=None): |
| 727 super(ReadOnlyCheckout, self).__init__() | 704 super(ReadOnlyCheckout, self).__init__() |
| 728 self.checkout = checkout | 705 self.checkout = checkout |
| 729 self.post_processors = (post_processors or []) + ( | 706 self.post_processors = (post_processors or []) + ( |
| 730 self.checkout.post_processors or []) | 707 self.checkout.post_processors or []) |
| 731 | 708 |
| (...skipping 15 matching lines...) Expand all Loading... |
| 747 def revisions(self, rev1, rev2): | 724 def revisions(self, rev1, rev2): |
| 748 return self.checkout.revisions(rev1, rev2) | 725 return self.checkout.revisions(rev1, rev2) |
| 749 | 726 |
| 750 @property | 727 @property |
| 751 def project_name(self): | 728 def project_name(self): |
| 752 return self.checkout.project_name | 729 return self.checkout.project_name |
| 753 | 730 |
| 754 @property | 731 @property |
| 755 def project_path(self): | 732 def project_path(self): |
| 756 return self.checkout.project_path | 733 return self.checkout.project_path |
| OLD | NEW |