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