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 |
(...skipping 548 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
559 commit_user, post_processors=None, base_ref=None): | 559 commit_user, post_processors=None, base_ref=None): |
560 super(GitCheckout, self).__init__(root_dir, project_name, post_processors) | 560 super(GitCheckout, self).__init__(root_dir, project_name, post_processors) |
561 self.base_ref = base_ref | 561 self.base_ref = base_ref |
562 self.git_url = git_url | 562 self.git_url = git_url |
563 self.commit_user = commit_user | 563 self.commit_user = commit_user |
564 self.remote_branch = remote_branch | 564 self.remote_branch = remote_branch |
565 # The working branch where patches will be applied. It will track the | 565 # The working branch where patches will be applied. It will track the |
566 # remote branch. | 566 # remote branch. |
567 self.working_branch = 'working_branch' | 567 self.working_branch = 'working_branch' |
568 # There is no reason to not hardcode origin. | 568 # There is no reason to not hardcode origin. |
569 self.pull_remote = 'origin' | 569 self.remote = 'origin' |
570 self.push_remote = 'upstream' | 570 # There is no reason to not hardcode master. |
| 571 self.master_branch = 'master' |
571 | 572 |
572 def prepare(self, revision): | 573 def prepare(self, revision): |
573 """Resets the git repository in a clean state. | 574 """Resets the git repository in a clean state. |
574 | 575 |
575 Checks it out if not present and deletes the working branch. | 576 Checks it out if not present and deletes the working branch. |
576 """ | 577 """ |
| 578 assert self.remote_branch |
577 assert self.git_url | 579 assert self.git_url |
578 assert self.remote_branch | |
579 | |
580 self._check_call_git( | |
581 ['cache', 'populate', self.git_url], timeout=FETCH_TIMEOUT, cwd=None) | |
582 cache_path = self._check_output_git( | |
583 ['cache', 'exists', self.git_url], cwd=None).strip() | |
584 | 580 |
585 if not os.path.isdir(self.project_path): | 581 if not os.path.isdir(self.project_path): |
586 # Clone the repo if the directory is not present. | 582 # Clone the repo if the directory is not present. |
| 583 logging.info( |
| 584 'Checking out %s in %s', self.project_name, self.project_path) |
587 self._check_call_git( | 585 self._check_call_git( |
588 ['clone', '--shared', cache_path, self.project_path], | 586 ['clone', self.git_url, '-b', self.remote_branch, self.project_path], |
589 cwd=None, timeout=FETCH_TIMEOUT) | 587 cwd=None, timeout=FETCH_TIMEOUT) |
590 self._call_git( | 588 else: |
591 ['config', 'remote.%s.url' % self.push_remote, self.git_url], | 589 # Throw away all uncommitted changes in the existing checkout. |
592 cwd=self.project_path) | 590 self._check_call_git(['checkout', self.remote_branch]) |
| 591 self._check_call_git( |
| 592 ['reset', '--hard', '--quiet', |
| 593 '%s/%s' % (self.remote, self.remote_branch)]) |
593 | 594 |
594 if not revision: | 595 if revision: |
595 revision = self.remote_branch | 596 try: |
| 597 # Look if the commit hash already exist. If so, we can skip a |
| 598 # 'git fetch' call. |
| 599 revision = self._check_output_git(['rev-parse', revision]) |
| 600 except subprocess.CalledProcessError: |
| 601 self._check_call_git( |
| 602 ['fetch', self.remote, self.remote_branch, '--quiet']) |
| 603 revision = self._check_output_git(['rev-parse', revision]) |
| 604 self._check_call_git(['checkout', '--force', '--quiet', revision]) |
| 605 else: |
| 606 branches, active = self._branches() |
| 607 if active != self.master_branch: |
| 608 self._check_call_git( |
| 609 ['checkout', '--force', '--quiet', self.master_branch]) |
| 610 self._sync_remote_branch() |
596 | 611 |
597 if not re.match(r'[0-9a-f]{40}$', revision, flags=re.IGNORECASE): | 612 if self.working_branch in branches: |
598 self._check_call_git(['fetch', self.pull_remote, revision]) | 613 self._call_git(['branch', '-D', self.working_branch]) |
599 revision = self.pull_remote + '/' + revision | 614 return self._get_head_commit_hash() |
600 | 615 |
601 self._check_call_git(['checkout', '--force', '--quiet', revision]) | 616 def _sync_remote_branch(self): |
602 self._call_git(['clean', '-fdx']) | 617 """Syncs the remote branch.""" |
603 | 618 # We do a 'git pull origin master:refs/remotes/origin/master' instead of |
604 branches, _ = self._branches() | 619 # 'git pull origin master' because from the manpage for git-pull: |
605 if self.working_branch in branches: | 620 # A parameter <ref> without a colon is equivalent to <ref>: when |
606 self._call_git(['branch', '-D', self.working_branch]) | 621 # pulling/fetching, so it merges <ref> into the current branch without |
607 | 622 # storing the remote branch anywhere locally. |
608 return self._get_head_commit_hash() | 623 remote_tracked_path = 'refs/remotes/%s/%s' % ( |
| 624 self.remote, self.remote_branch) |
| 625 self._check_call_git( |
| 626 ['pull', self.remote, |
| 627 '%s:%s' % (self.remote_branch, remote_tracked_path), |
| 628 '--quiet']) |
609 | 629 |
610 def _get_head_commit_hash(self): | 630 def _get_head_commit_hash(self): |
611 """Gets the current revision (in unicode) from the local branch.""" | 631 """Gets the current revision (in unicode) from the local branch.""" |
612 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip()) | 632 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip()) |
613 | 633 |
614 def apply_patch(self, patches, post_processors=None, verbose=False, | 634 def apply_patch(self, patches, post_processors=None, verbose=False, |
615 name=None, email=None): | 635 name=None, email=None): |
616 """Applies a patch on 'working_branch' and switches to it. | 636 """Applies a patch on 'working_branch' and switches to it. |
617 | 637 |
618 Also commits the changes on the local branch. | 638 Also commits the changes on the local branch. |
619 | 639 |
620 Ignores svn properties and raise an exception on unexpected ones. | 640 Ignores svn properties and raise an exception on unexpected ones. |
621 """ | 641 """ |
622 post_processors = post_processors or self.post_processors or [] | 642 post_processors = post_processors or self.post_processors or [] |
623 # It this throws, the checkout is corrupted. Maybe worth deleting it and | 643 # It this throws, the checkout is corrupted. Maybe worth deleting it and |
624 # trying again? | 644 # trying again? |
625 if self.remote_branch: | 645 if self.remote_branch: |
626 self._check_call_git( | 646 self._check_call_git( |
627 ['checkout', '-b', self.working_branch, | 647 ['checkout', '-b', self.working_branch, '-t', self.remote_branch, |
628 '-t', '%s/%s' % (self.pull_remote, self.remote_branch), | |
629 '--quiet']) | 648 '--quiet']) |
630 | 649 |
631 for index, p in enumerate(patches): | 650 for index, p in enumerate(patches): |
632 stdout = [] | 651 stdout = [] |
633 try: | 652 try: |
634 filepath = os.path.join(self.project_path, p.filename) | 653 filepath = os.path.join(self.project_path, p.filename) |
635 if p.is_delete: | 654 if p.is_delete: |
636 if (not os.path.exists(filepath) and | 655 if (not os.path.exists(filepath) and |
637 any(p1.source_filename == p.filename for p1 in patches[0:index])): | 656 any(p1.source_filename == p.filename for p1 in patches[0:index])): |
638 # The file was already deleted if a prior patch with file rename | 657 # The file was already deleted if a prior patch with file rename |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
694 # index. | 713 # index. |
695 cmd = ['commit', '-m', 'Committed patch'] | 714 cmd = ['commit', '-m', 'Committed patch'] |
696 if name and email: | 715 if name and email: |
697 cmd = ['-c', 'user.email=%s' % email, '-c', 'user.name=%s' % name] + cmd | 716 cmd = ['-c', 'user.email=%s' % email, '-c', 'user.name=%s' % name] + cmd |
698 if verbose: | 717 if verbose: |
699 cmd.append('--verbose') | 718 cmd.append('--verbose') |
700 self._check_call_git(cmd) | 719 self._check_call_git(cmd) |
701 if self.base_ref: | 720 if self.base_ref: |
702 base_ref = self.base_ref | 721 base_ref = self.base_ref |
703 else: | 722 else: |
704 base_ref = '%s/%s' % (self.pull_remote, self.remote_branch) | 723 base_ref = '%s/%s' % (self.remote, |
| 724 self.remote_branch or self.master_branch) |
705 found_files = self._check_output_git( | 725 found_files = self._check_output_git( |
706 ['diff', base_ref, | 726 ['diff', base_ref, |
707 '--name-only']).splitlines(False) | 727 '--name-only']).splitlines(False) |
708 assert sorted(patches.filenames) == sorted(found_files), ( | 728 assert sorted(patches.filenames) == sorted(found_files), ( |
709 sorted(patches.filenames), sorted(found_files)) | 729 sorted(patches.filenames), sorted(found_files)) |
710 | 730 |
711 def commit(self, commit_message, user): | 731 def commit(self, commit_message, user): |
712 """Commits, updates the commit message and pushes.""" | 732 """Commits, updates the commit message and pushes.""" |
713 assert self.commit_user | 733 assert self.commit_user |
714 assert isinstance(commit_message, unicode) | 734 assert isinstance(commit_message, unicode) |
715 current_branch = self._check_output_git( | 735 current_branch = self._check_output_git( |
716 ['rev-parse', '--abbrev-ref', 'HEAD']).strip() | 736 ['rev-parse', '--abbrev-ref', 'HEAD']).strip() |
717 assert current_branch == self.working_branch | 737 assert current_branch == self.working_branch |
718 | 738 |
719 commit_cmd = ['commit', '--amend', '-m', commit_message] | 739 commit_cmd = ['commit', '--amend', '-m', commit_message] |
720 if user and user != self.commit_user: | 740 if user and user != self.commit_user: |
721 # We do not have the first or last name of the user, grab the username | 741 # We do not have the first or last name of the user, grab the username |
722 # from the email and call it the original author's name. | 742 # from the email and call it the original author's name. |
723 # TODO(rmistry): Do not need the below if user is already in | 743 # TODO(rmistry): Do not need the below if user is already in |
724 # "Name <email>" format. | 744 # "Name <email>" format. |
725 name = user.split('@')[0] | 745 name = user.split('@')[0] |
726 commit_cmd.extend(['--author', '%s <%s>' % (name, user)]) | 746 commit_cmd.extend(['--author', '%s <%s>' % (name, user)]) |
727 self._check_call_git(commit_cmd) | 747 self._check_call_git(commit_cmd) |
728 | 748 |
729 # Push to the remote repository. | 749 # Push to the remote repository. |
730 self._check_call_git( | 750 self._check_call_git( |
731 ['push', self.push_remote, | 751 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch), |
732 '%s:%s' % (self.working_branch, self.remote_branch), | |
733 '--force', '--quiet']) | 752 '--force', '--quiet']) |
734 # Get the revision after the push. | 753 # Get the revision after the push. |
735 revision = self._get_head_commit_hash() | 754 revision = self._get_head_commit_hash() |
736 # Switch back to the remote_branch. | 755 # Switch back to the remote_branch and sync it. |
737 self._check_call_git(['cache', 'populate', self.git_url]) | 756 self._check_call_git(['checkout', self.remote_branch]) |
738 self._check_call_git(['fetch', self.pull_remote]) | 757 self._sync_remote_branch() |
739 self._check_call_git(['checkout', '--force', '--quiet', | |
740 '%s/%s' % (self.pull_remote, self.remote_branch)]) | |
741 # Delete the working branch since we are done with it. | 758 # Delete the working branch since we are done with it. |
742 self._check_call_git(['branch', '-D', self.working_branch]) | 759 self._check_call_git(['branch', '-D', self.working_branch]) |
743 | 760 |
744 return revision | 761 return revision |
745 | 762 |
746 def _check_call_git(self, args, **kwargs): | 763 def _check_call_git(self, args, **kwargs): |
747 kwargs.setdefault('cwd', self.project_path) | 764 kwargs.setdefault('cwd', self.project_path) |
748 kwargs.setdefault('stdout', self.VOID) | 765 kwargs.setdefault('stdout', self.VOID) |
749 kwargs.setdefault('timeout', GLOBAL_TIMEOUT) | 766 kwargs.setdefault('timeout', GLOBAL_TIMEOUT) |
750 return subprocess2.check_call_out(['git'] + args, **kwargs) | 767 return subprocess2.check_call_out(['git'] + args, **kwargs) |
(...skipping 19 matching lines...) Expand all Loading... |
770 for l in out: | 787 for l in out: |
771 if l.startswith('*'): | 788 if l.startswith('*'): |
772 active = l[2:] | 789 active = l[2:] |
773 break | 790 break |
774 return branches, active | 791 return branches, active |
775 | 792 |
776 def revisions(self, rev1, rev2): | 793 def revisions(self, rev1, rev2): |
777 """Returns the number of actual commits between both hash.""" | 794 """Returns the number of actual commits between both hash.""" |
778 self._fetch_remote() | 795 self._fetch_remote() |
779 | 796 |
780 rev2 = rev2 or '%s/%s' % (self.pull_remote, self.remote_branch) | 797 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch) |
781 # Revision range is ]rev1, rev2] and ordering matters. | 798 # Revision range is ]rev1, rev2] and ordering matters. |
782 try: | 799 try: |
783 out = self._check_output_git( | 800 out = self._check_output_git( |
784 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)]) | 801 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)]) |
785 except subprocess.CalledProcessError: | 802 except subprocess.CalledProcessError: |
786 return None | 803 return None |
787 return len(out.splitlines()) | 804 return len(out.splitlines()) |
788 | 805 |
789 def _fetch_remote(self): | 806 def _fetch_remote(self): |
790 """Fetches the remote without rebasing.""" | 807 """Fetches the remote without rebasing.""" |
791 # git fetch is always verbose even with -q, so redirect its output. | 808 # git fetch is always verbose even with -q, so redirect its output. |
792 self._check_output_git(['fetch', self.pull_remote, self.remote_branch], | 809 self._check_output_git(['fetch', self.remote, self.remote_branch], |
793 timeout=FETCH_TIMEOUT) | 810 timeout=FETCH_TIMEOUT) |
794 | 811 |
795 | 812 |
796 class ReadOnlyCheckout(object): | 813 class ReadOnlyCheckout(object): |
797 """Converts a checkout into a read-only one.""" | 814 """Converts a checkout into a read-only one.""" |
798 def __init__(self, checkout, post_processors=None): | 815 def __init__(self, checkout, post_processors=None): |
799 super(ReadOnlyCheckout, self).__init__() | 816 super(ReadOnlyCheckout, self).__init__() |
800 self.checkout = checkout | 817 self.checkout = checkout |
801 self.post_processors = (post_processors or []) + ( | 818 self.post_processors = (post_processors or []) + ( |
802 self.checkout.post_processors or []) | 819 self.checkout.post_processors or []) |
(...skipping 17 matching lines...) Expand all Loading... |
820 def revisions(self, rev1, rev2): | 837 def revisions(self, rev1, rev2): |
821 return self.checkout.revisions(rev1, rev2) | 838 return self.checkout.revisions(rev1, rev2) |
822 | 839 |
823 @property | 840 @property |
824 def project_name(self): | 841 def project_name(self): |
825 return self.checkout.project_name | 842 return self.checkout.project_name |
826 | 843 |
827 @property | 844 @property |
828 def project_path(self): | 845 def project_path(self): |
829 return self.checkout.project_path | 846 return self.checkout.project_path |
OLD | NEW |