OLD | NEW |
---|---|
1 # coding=utf8 | 1 # coding=utf8 |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 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 from __future__ import with_statement | 10 from __future__ import with_statement |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
73 self.project_path = os.path.join(self.root_dir, self.project_name) | 73 self.project_path = os.path.join(self.root_dir, self.project_name) |
74 # Only used for logging purposes. | 74 # Only used for logging purposes. |
75 self._last_seen_revision = None | 75 self._last_seen_revision = None |
76 self.post_processors = None | 76 self.post_processors = None |
77 assert self.root_dir | 77 assert self.root_dir |
78 assert self.project_path | 78 assert self.project_path |
79 | 79 |
80 def get_settings(self, key): | 80 def get_settings(self, key): |
81 return get_code_review_setting(self.project_path, key) | 81 return get_code_review_setting(self.project_path, key) |
82 | 82 |
83 def prepare(self): | 83 def prepare(self, revision): |
84 """Checks out a clean copy of the tree and removes any local modification. | 84 """Checks out a clean copy of the tree and removes any local modification. |
85 | 85 |
86 This function shouldn't throw unless the remote repository is inaccessible, | 86 This function shouldn't throw unless the remote repository is inaccessible, |
87 there is no free disk space or hard issues like that. | 87 there is no free disk space or hard issues like that. |
88 | |
89 Args: | |
90 revision: The revision it should sync to, SCM specific. | |
88 """ | 91 """ |
89 raise NotImplementedError() | 92 raise NotImplementedError() |
90 | 93 |
91 def apply_patch(self, patches): | 94 def apply_patch(self, patches): |
92 """Applies a patch and returns the list of modified files. | 95 """Applies a patch and returns the list of modified files. |
93 | 96 |
94 This function should throw patch.UnsupportedPatchFormat or | 97 This function should throw patch.UnsupportedPatchFormat or |
95 PatchApplicationFailed when relevant. | 98 PatchApplicationFailed when relevant. |
96 | 99 |
97 Args: | 100 Args: |
98 patches: patch.PatchSet object. | 101 patches: patch.PatchSet object. |
99 """ | 102 """ |
100 raise NotImplementedError() | 103 raise NotImplementedError() |
101 | 104 |
102 def commit(self, commit_message, user): | 105 def commit(self, commit_message, user): |
103 """Commits the patch upstream, while impersonating 'user'.""" | 106 """Commits the patch upstream, while impersonating 'user'.""" |
104 raise NotImplementedError() | 107 raise NotImplementedError() |
105 | 108 |
106 | 109 |
107 class RawCheckout(CheckoutBase): | 110 class RawCheckout(CheckoutBase): |
108 """Used to apply a patch locally without any intent to commit it. | 111 """Used to apply a patch locally without any intent to commit it. |
109 | 112 |
110 To be used by the try server. | 113 To be used by the try server. |
111 """ | 114 """ |
112 def prepare(self): | 115 def prepare(self, revision): |
113 """Stubbed out.""" | 116 """Stubbed out.""" |
114 pass | 117 pass |
115 | 118 |
116 def apply_patch(self, patches): | 119 def apply_patch(self, patches): |
117 """Ignores svn properties.""" | 120 """Ignores svn properties.""" |
118 for p in patches: | 121 for p in patches: |
119 try: | 122 try: |
120 stdout = '' | 123 stdout = '' |
121 filename = os.path.join(self.project_path, p.filename) | 124 filename = os.path.join(self.project_path, p.filename) |
122 if p.is_delete: | 125 if p.is_delete: |
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
237 class SvnCheckout(CheckoutBase, SvnMixIn): | 240 class SvnCheckout(CheckoutBase, SvnMixIn): |
238 """Manages a subversion checkout.""" | 241 """Manages a subversion checkout.""" |
239 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url, | 242 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url, |
240 post_processors=None): | 243 post_processors=None): |
241 super(SvnCheckout, self).__init__(root_dir, project_name, post_processors) | 244 super(SvnCheckout, self).__init__(root_dir, project_name, post_processors) |
242 self.commit_user = commit_user | 245 self.commit_user = commit_user |
243 self.commit_pwd = commit_pwd | 246 self.commit_pwd = commit_pwd |
244 self.svn_url = svn_url | 247 self.svn_url = svn_url |
245 assert bool(self.commit_user) >= bool(self.commit_pwd) | 248 assert bool(self.commit_user) >= bool(self.commit_pwd) |
246 | 249 |
247 def prepare(self): | 250 def prepare(self, revision): |
248 # Will checkout if the directory is not present. | 251 # Will checkout if the directory is not present. |
249 assert self.svn_url | 252 assert self.svn_url |
250 if not os.path.isdir(self.project_path): | 253 if not os.path.isdir(self.project_path): |
251 logging.info('Checking out %s in %s' % | 254 logging.info('Checking out %s in %s' % |
252 (self.project_name, self.project_path)) | 255 (self.project_name, self.project_path)) |
253 revision = self._revert() | 256 return self._revert(revision) |
254 if revision != self._last_seen_revision: | |
255 logging.info('Updated at revision %d' % revision) | |
256 self._last_seen_revision = revision | |
257 return revision | |
258 | 257 |
259 def apply_patch(self, patches): | 258 def apply_patch(self, patches): |
260 for p in patches: | 259 for p in patches: |
261 try: | 260 try: |
262 # It is important to use credentials=False otherwise credentials could | 261 # It is important to use credentials=False otherwise credentials could |
263 # leak in the error message. Credentials are not necessary here for the | 262 # leak in the error message. Credentials are not necessary here for the |
264 # following commands anyway. | 263 # following commands anyway. |
265 stdout = '' | 264 stdout = '' |
266 if p.is_delete: | 265 if p.is_delete: |
267 stdout += self._check_output_svn( | 266 stdout += self._check_output_svn( |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
344 finally: | 343 finally: |
345 os.remove(commit_filename) | 344 os.remove(commit_filename) |
346 lines = filter(None, out.splitlines()) | 345 lines = filter(None, out.splitlines()) |
347 match = re.match(r'^Committed revision (\d+).$', lines[-1]) | 346 match = re.match(r'^Committed revision (\d+).$', lines[-1]) |
348 if not match: | 347 if not match: |
349 raise PatchApplicationFailed( | 348 raise PatchApplicationFailed( |
350 None, | 349 None, |
351 'Couldn\'t make sense out of svn commit message:\n' + out) | 350 'Couldn\'t make sense out of svn commit message:\n' + out) |
352 return int(match.group(1)) | 351 return int(match.group(1)) |
353 | 352 |
354 def _revert(self): | 353 def _revert(self, revision): |
355 """Reverts local modifications or checks out if the directory is not | 354 """Reverts local modifications or checks out if the directory is not |
356 present. Use depot_tools's functionality to do this. | 355 present. Use depot_tools's functionality to do this. |
357 """ | 356 """ |
358 flags = ['--ignore-externals'] | 357 flags = ['--ignore-externals'] |
358 if revision: | |
359 flags.extend(['--revision', str(revision)]) | |
359 if not os.path.isdir(self.project_path): | 360 if not os.path.isdir(self.project_path): |
360 logging.info( | 361 logging.info( |
361 'Directory %s is not present, checking it out.' % self.project_path) | 362 'Directory %s is not present, checking it out.' % self.project_path) |
362 self._check_call_svn( | 363 self._check_call_svn( |
363 ['checkout', self.svn_url, self.project_path] + flags, cwd=None) | 364 ['checkout', self.svn_url, self.project_path] + flags, cwd=None) |
364 else: | 365 else: |
365 scm.SVN.Revert(self.project_path) | 366 scm.SVN.Revert(self.project_path) |
366 # Revive files that were deleted in scm.SVN.Revert(). | 367 # Revive files that were deleted in scm.SVN.Revert(). |
367 self._check_call_svn(['update', '--force'] + flags) | 368 self._check_call_svn(['update', '--force'] + flags) |
369 return self._get_revision() | |
368 | 370 |
371 def _get_revision(self): | |
369 out = self._check_output_svn(['info', '.']) | 372 out = self._check_output_svn(['info', '.']) |
370 return int(self._parse_svn_info(out, 'revision')) | 373 revision = int(self._parse_svn_info(out, 'revision')) |
374 if revision != self._last_seen_revision: | |
375 logging.info('Updated at revision %d' % revision) | |
Dirk Pranke
2011/06/10 21:07:45
Nit: "Updated to revision" ?
| |
376 self._last_seen_revision = revision | |
377 return revision | |
371 | 378 |
372 | 379 |
373 class GitCheckoutBase(CheckoutBase): | 380 class GitCheckoutBase(CheckoutBase): |
374 """Base class for git checkout. Not to be used as-is.""" | 381 """Base class for git checkout. Not to be used as-is.""" |
375 def __init__(self, root_dir, project_name, remote_branch, | 382 def __init__(self, root_dir, project_name, remote_branch, |
376 post_processors=None): | 383 post_processors=None): |
377 super(GitCheckoutBase, self).__init__( | 384 super(GitCheckoutBase, self).__init__( |
378 root_dir, project_name, post_processors) | 385 root_dir, project_name, post_processors) |
379 # There is no reason to not hardcode it. | 386 # There is no reason to not hardcode it. |
380 self.remote = 'origin' | 387 self.remote = 'origin' |
381 self.remote_branch = remote_branch | 388 self.remote_branch = remote_branch |
382 self.working_branch = 'working_branch' | 389 self.working_branch = 'working_branch' |
383 | 390 |
384 def prepare(self): | 391 def prepare(self, revision): |
385 """Resets the git repository in a clean state. | 392 """Resets the git repository in a clean state. |
386 | 393 |
387 Checks it out if not present and deletes the working branch. | 394 Checks it out if not present and deletes the working branch. |
388 """ | 395 """ |
389 assert self.remote_branch | 396 assert self.remote_branch |
390 assert os.path.isdir(self.project_path) | 397 assert os.path.isdir(self.project_path) |
391 self._check_call_git(['reset', '--hard', '--quiet']) | 398 self._check_call_git(['reset', '--hard', '--quiet']) |
392 branches, active = self._branches() | 399 if revision: |
393 if active != 'master': | 400 try: |
394 self._check_call_git(['checkout', 'master', '--force', '--quiet']) | 401 revision = self._check_output_git(['rev-parse', revision]) |
395 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) | 402 except subprocess.CalledProcessError: |
396 if self.working_branch in branches: | 403 self._check_call_git( |
397 self._call_git(['branch', '-D', self.working_branch]) | 404 ['fetch', self.remote, self.remote_branch, '--quiet']) |
405 revision = self._check_output_git(['rev-parse', revision]) | |
406 self._check_call_git(['checkout', '--force', '--quiet', revision]) | |
407 else: | |
408 branches, active = self._branches() | |
409 if active != 'master': | |
410 self._check_call_git(['checkout', '--force', '--quiet', 'master']) | |
411 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) | |
412 if self.working_branch in branches: | |
413 self._call_git(['branch', '-D', self.working_branch]) | |
398 | 414 |
399 def apply_patch(self, patches): | 415 def apply_patch(self, patches): |
400 """Applies a patch on 'working_branch' and switch to it. | 416 """Applies a patch on 'working_branch' and switch to it. |
401 | 417 |
402 Also commits the changes on the local branch. | 418 Also commits the changes on the local branch. |
403 | 419 |
404 Ignores svn properties and raise an exception on unexpected ones. | 420 Ignores svn properties and raise an exception on unexpected ones. |
405 """ | 421 """ |
406 # It this throws, the checkout is corrupted. Maybe worth deleting it and | 422 # It this throws, the checkout is corrupted. Maybe worth deleting it and |
407 # trying again? | 423 # trying again? |
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
503 self.commit_user = commit_user | 519 self.commit_user = commit_user |
504 self.commit_pwd = commit_pwd | 520 self.commit_pwd = commit_pwd |
505 # svn_url in this case is the root of the svn repository. | 521 # svn_url in this case is the root of the svn repository. |
506 self.svn_url = svn_url | 522 self.svn_url = svn_url |
507 self.trunk = trunk | 523 self.trunk = trunk |
508 assert bool(self.commit_user) >= bool(self.commit_pwd) | 524 assert bool(self.commit_user) >= bool(self.commit_pwd) |
509 assert self.svn_url | 525 assert self.svn_url |
510 assert self.trunk | 526 assert self.trunk |
511 self._cache_svn_auth() | 527 self._cache_svn_auth() |
512 | 528 |
513 def prepare(self): | 529 def prepare(self, revision): |
514 """Resets the git repository in a clean state.""" | 530 """Resets the git repository in a clean state.""" |
515 self._check_call_git(['reset', '--hard', '--quiet']) | 531 self._check_call_git(['reset', '--hard', '--quiet']) |
516 branches, active = self._branches() | 532 if revision: |
517 if active != 'master': | 533 try: |
518 if not 'master' in branches: | 534 revision = self._check_output_git( |
535 ['svn', 'find-rev', 'r%d' % revision]) | |
536 except subprocess.CalledProcessError: | |
519 self._check_call_git( | 537 self._check_call_git( |
520 ['checkout', '--quiet', '-b', 'master', | 538 ['fetch', self.remote, self.remote_branch, '--quiet']) |
521 '%s/%s' % (self.remote, self.remote_branch)]) | 539 revision = self._check_output_git( |
522 else: | 540 ['svn', 'find-rev', 'r%d' % revision]) |
523 self._check_call_git(['checkout', 'master', '--force', '--quiet']) | 541 super(GitSvnCheckoutBase, self).prepare(revision) |
524 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it. | 542 else: |
525 self._check_call_git_svn(['fetch', '--quiet', '--quiet']) | 543 branches, active = self._branches() |
526 self._check_call_git( | 544 if active != 'master': |
527 ['rebase', '--quiet', '--quiet', | 545 if not 'master' in branches: |
528 '%s/%s' % (self.remote, self.remote_branch)]) | 546 self._check_call_git( |
529 if self.working_branch in branches: | 547 ['checkout', '--quiet', '-b', 'master', |
530 self._call_git(['branch', '-D', self.working_branch]) | 548 '%s/%s' % (self.remote, self.remote_branch)]) |
531 return int(self._git_svn_info('revision')) | 549 else: |
550 self._check_call_git(['checkout', 'master', '--force', '--quiet']) | |
551 # git svn rebase --quiet --quiet doesn't work, use two steps to silence | |
552 # it. | |
553 self._check_call_git_svn(['fetch', '--quiet', '--quiet']) | |
554 self._check_call_git( | |
555 ['rebase', '--quiet', '--quiet', | |
556 '%s/%s' % (self.remote, self.remote_branch)]) | |
557 if self.working_branch in branches: | |
558 self._call_git(['branch', '-D', self.working_branch]) | |
559 return self._get_revision() | |
532 | 560 |
533 def _git_svn_info(self, key): | 561 def _git_svn_info(self, key): |
534 """Calls git svn info. This doesn't support nor need --config-dir.""" | 562 """Calls git svn info. This doesn't support nor need --config-dir.""" |
535 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key) | 563 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key) |
536 | 564 |
537 def commit(self, commit_message, user): | 565 def commit(self, commit_message, user): |
538 """Commits a patch.""" | 566 """Commits a patch.""" |
539 logging.info('Committing patch for %s' % user) | 567 logging.info('Committing patch for %s' % user) |
540 # Fix the commit message and author. It returns the git hash, which we | 568 # Fix the commit message and author. It returns the git hash, which we |
541 # ignore unless it's None. | 569 # ignore unless it's None. |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
588 root_dir, project_name, remote_branch, | 616 root_dir, project_name, remote_branch, |
589 commit_user, commit_pwd, | 617 commit_user, commit_pwd, |
590 svn_url, trunk, git_url, post_processors=None): | 618 svn_url, trunk, git_url, post_processors=None): |
591 super(GitSvnPremadeCheckout, self).__init__( | 619 super(GitSvnPremadeCheckout, self).__init__( |
592 root_dir, project_name, remote_branch, | 620 root_dir, project_name, remote_branch, |
593 commit_user, commit_pwd, | 621 commit_user, commit_pwd, |
594 svn_url, trunk, post_processors) | 622 svn_url, trunk, post_processors) |
595 self.git_url = git_url | 623 self.git_url = git_url |
596 assert self.git_url | 624 assert self.git_url |
597 | 625 |
598 def prepare(self): | 626 def prepare(self, revision): |
599 """Creates the initial checkout for the repo.""" | 627 """Creates the initial checkout for the repo.""" |
600 if not os.path.isdir(self.project_path): | 628 if not os.path.isdir(self.project_path): |
601 logging.info('Checking out %s in %s' % | 629 logging.info('Checking out %s in %s' % |
602 (self.project_name, self.project_path)) | 630 (self.project_name, self.project_path)) |
603 assert self.remote == 'origin' | 631 assert self.remote == 'origin' |
604 # self.project_path doesn't exist yet. | 632 # self.project_path doesn't exist yet. |
605 self._check_call_git( | 633 self._check_call_git( |
606 ['clone', self.git_url, self.project_name, '--quiet'], | 634 ['clone', self.git_url, self.project_name, '--quiet'], |
607 cwd=self.root_dir, | 635 cwd=self.root_dir, |
608 stderr=subprocess2.STDOUT) | 636 stderr=subprocess2.STDOUT) |
609 try: | 637 try: |
610 configured_svn_url = self._check_output_git( | 638 configured_svn_url = self._check_output_git( |
611 ['config', 'svn-remote.svn.url']).strip() | 639 ['config', 'svn-remote.svn.url']).strip() |
612 except subprocess.CalledProcessError: | 640 except subprocess.CalledProcessError: |
613 configured_svn_url = '' | 641 configured_svn_url = '' |
614 | 642 |
615 if configured_svn_url.strip() != self.svn_url: | 643 if configured_svn_url.strip() != self.svn_url: |
616 self._check_call_git_svn( | 644 self._check_call_git_svn( |
617 ['init', | 645 ['init', |
618 '--prefix', self.remote + '/', | 646 '--prefix', self.remote + '/', |
619 '-T', self.trunk, | 647 '-T', self.trunk, |
620 self.svn_url]) | 648 self.svn_url]) |
621 self._check_call_git_svn(['fetch']) | 649 self._check_call_git_svn(['fetch']) |
622 super(GitSvnPremadeCheckout, self).prepare() | 650 return super(GitSvnPremadeCheckout, self).prepare(revision) |
623 return self._get_revision() | |
624 | 651 |
625 | 652 |
626 class GitSvnCheckout(GitSvnCheckoutBase): | 653 class GitSvnCheckout(GitSvnCheckoutBase): |
627 """Manages a git-svn clone. | 654 """Manages a git-svn clone. |
628 | 655 |
629 Using git-svn hides some of the complexity of using a svn checkout. | 656 Using git-svn hides some of the complexity of using a svn checkout. |
630 """ | 657 """ |
631 def __init__(self, | 658 def __init__(self, |
632 root_dir, project_name, | 659 root_dir, project_name, |
633 commit_user, commit_pwd, | 660 commit_user, commit_pwd, |
634 svn_url, trunk, post_processors=None): | 661 svn_url, trunk, post_processors=None): |
635 super(GitSvnCheckout, self).__init__( | 662 super(GitSvnCheckout, self).__init__( |
636 root_dir, project_name, 'trunk', | 663 root_dir, project_name, 'trunk', |
637 commit_user, commit_pwd, | 664 commit_user, commit_pwd, |
638 svn_url, trunk, post_processors) | 665 svn_url, trunk, post_processors) |
639 | 666 |
640 def prepare(self): | 667 def prepare(self, revision): |
641 """Creates the initial checkout for the repo.""" | 668 """Creates the initial checkout for the repo.""" |
669 assert not revision, 'Implement revision if necessary' | |
642 if not os.path.isdir(self.project_path): | 670 if not os.path.isdir(self.project_path): |
643 logging.info('Checking out %s in %s' % | 671 logging.info('Checking out %s in %s' % |
644 (self.project_name, self.project_path)) | 672 (self.project_name, self.project_path)) |
645 # TODO: Create a shallow clone. | 673 # TODO: Create a shallow clone. |
646 # self.project_path doesn't exist yet. | 674 # self.project_path doesn't exist yet. |
647 self._check_call_git_svn( | 675 self._check_call_git_svn( |
648 ['clone', | 676 ['clone', |
649 '--prefix', self.remote + '/', | 677 '--prefix', self.remote + '/', |
650 '-T', self.trunk, | 678 '-T', self.trunk, |
651 self.svn_url, self.project_path, | 679 self.svn_url, self.project_path, |
652 '--quiet'], | 680 '--quiet'], |
653 cwd=self.root_dir, | 681 cwd=self.root_dir, |
654 stderr=subprocess2.STDOUT) | 682 stderr=subprocess2.STDOUT) |
655 super(GitSvnCheckout, self).prepare() | 683 return super(GitSvnCheckout, self).prepare(revision) |
656 return self._get_revision() | |
657 | 684 |
658 | 685 |
659 class ReadOnlyCheckout(object): | 686 class ReadOnlyCheckout(object): |
660 """Converts a checkout into a read-only one.""" | 687 """Converts a checkout into a read-only one.""" |
661 def __init__(self, checkout): | 688 def __init__(self, checkout): |
662 self.checkout = checkout | 689 self.checkout = checkout |
663 | 690 |
664 def prepare(self): | 691 def prepare(self, revision): |
665 return self.checkout.prepare() | 692 return self.checkout.prepare(revision) |
666 | 693 |
667 def get_settings(self, key): | 694 def get_settings(self, key): |
668 return self.checkout.get_settings(key) | 695 return self.checkout.get_settings(key) |
669 | 696 |
670 def apply_patch(self, patches): | 697 def apply_patch(self, patches): |
671 return self.checkout.apply_patch(patches) | 698 return self.checkout.apply_patch(patches) |
672 | 699 |
673 def commit(self, message, user): # pylint: disable=R0201 | 700 def commit(self, message, user): # pylint: disable=R0201 |
674 logging.info('Would have committed for %s with message: %s' % ( | 701 logging.info('Would have committed for %s with message: %s' % ( |
675 user, message)) | 702 user, message)) |
676 return 'FAKE' | 703 return 'FAKE' |
677 | 704 |
678 @property | 705 @property |
679 def project_name(self): | 706 def project_name(self): |
680 return self.checkout.project_name | 707 return self.checkout.project_name |
681 | 708 |
682 @property | 709 @property |
683 def project_path(self): | 710 def project_path(self): |
684 return self.checkout.project_path | 711 return self.checkout.project_path |
OLD | NEW |