Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(54)

Side by Side Diff: checkout.py

Issue 22794015: Completing implementation of GitCheckout in depot_tools (Closed) Base URL: http://src.chromium.org/svn/trunk/tools/depot_tools/
Patch Set: Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « apply_issue.py ('k') | tests/checkout_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 113 matching lines...) Expand 10 before | Expand all | Expand 10 after
124 """Checks out a clean copy of the tree and removes any local modification. 124 """Checks out a clean copy of the tree and removes any local modification.
125 125
126 This function shouldn't throw unless the remote repository is inaccessible, 126 This function shouldn't throw unless the remote repository is inaccessible,
127 there is no free disk space or hard issues like that. 127 there is no free disk space or hard issues like that.
128 128
129 Args: 129 Args:
130 revision: The revision it should sync to, SCM specific. 130 revision: The revision it should sync to, SCM specific.
131 """ 131 """
132 raise NotImplementedError() 132 raise NotImplementedError()
133 133
134 def apply_patch(self, patches, post_processors=None, verbose=False): 134 def apply_patch(self, patches, post_processors=None, verbose=False,
135 revert=False):
135 """Applies a patch and returns the list of modified files. 136 """Applies a patch and returns the list of modified files.
136 137
137 This function should throw patch.UnsupportedPatchFormat or 138 This function should throw patch.UnsupportedPatchFormat or
138 PatchApplicationFailed when relevant. 139 PatchApplicationFailed when relevant.
139 140
140 Args: 141 Args:
141 patches: patch.PatchSet object. 142 patches: patch.PatchSet object.
142 """ 143 """
143 raise NotImplementedError() 144 raise NotImplementedError()
144 145
(...skipping 13 matching lines...) Expand all
158 159
159 class RawCheckout(CheckoutBase): 160 class RawCheckout(CheckoutBase):
160 """Used to apply a patch locally without any intent to commit it. 161 """Used to apply a patch locally without any intent to commit it.
161 162
162 To be used by the try server. 163 To be used by the try server.
163 """ 164 """
164 def prepare(self, revision): 165 def prepare(self, revision):
165 """Stubbed out.""" 166 """Stubbed out."""
166 pass 167 pass
167 168
168 def apply_patch(self, patches, post_processors=None, verbose=False): 169 def apply_patch(self, patches, post_processors=None, verbose=False,
170 unused_revert=False):
169 """Ignores svn properties.""" 171 """Ignores svn properties."""
170 post_processors = post_processors or self.post_processors or [] 172 post_processors = post_processors or self.post_processors or []
171 for p in patches: 173 for p in patches:
172 stdout = [] 174 stdout = []
173 try: 175 try:
174 filepath = os.path.join(self.project_path, p.filename) 176 filepath = os.path.join(self.project_path, p.filename)
175 if p.is_delete: 177 if p.is_delete:
176 os.remove(filepath) 178 os.remove(filepath)
177 stdout.append('Deleted.') 179 stdout.append('Deleted.')
178 else: 180 else:
(...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after
342 assert bool(self.commit_user) >= bool(self.commit_pwd) 344 assert bool(self.commit_user) >= bool(self.commit_pwd)
343 345
344 def prepare(self, revision): 346 def prepare(self, revision):
345 # Will checkout if the directory is not present. 347 # Will checkout if the directory is not present.
346 assert self.svn_url 348 assert self.svn_url
347 if not os.path.isdir(self.project_path): 349 if not os.path.isdir(self.project_path):
348 logging.info('Checking out %s in %s' % 350 logging.info('Checking out %s in %s' %
349 (self.project_name, self.project_path)) 351 (self.project_name, self.project_path))
350 return self._revert(revision) 352 return self._revert(revision)
351 353
352 def apply_patch(self, patches, post_processors=None, verbose=False): 354 def apply_patch(self, patches, post_processors=None, verbose=False,
355 unused_revert=False):
M-A Ruel 2013/09/02 20:35:09 Keep revert=False for compatibility. Why not: ass
rmistry 2013/09/03 12:40:36 Yes will do in a separate CL, see the comment belo
353 post_processors = post_processors or self.post_processors or [] 356 post_processors = post_processors or self.post_processors or []
354 for p in patches: 357 for p in patches:
355 stdout = [] 358 stdout = []
356 try: 359 try:
357 filepath = os.path.join(self.project_path, p.filename) 360 filepath = os.path.join(self.project_path, p.filename)
358 # It is important to use credentials=False otherwise credentials could 361 # It is important to use credentials=False otherwise credentials could
359 # leak in the error message. Credentials are not necessary here for the 362 # leak in the error message. Credentials are not necessary here for the
360 # following commands anyway. 363 # following commands anyway.
361 if p.is_delete: 364 if p.is_delete:
362 stdout.append(self._check_output_svn( 365 stdout.append(self._check_output_svn(
(...skipping 180 matching lines...) Expand 10 before | Expand all | Expand 10 after
543 # the order specified. 546 # the order specified.
544 try: 547 try:
545 out = self._check_output_svn( 548 out = self._check_output_svn(
546 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)]) 549 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
547 except subprocess.CalledProcessError: 550 except subprocess.CalledProcessError:
548 return None 551 return None
549 # Ignore the '----' lines. 552 # Ignore the '----' lines.
550 return len([l for l in out.splitlines() if l.startswith('r')]) - 1 553 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
551 554
552 555
553 class GitCheckoutBase(CheckoutBase): 556 class GitCheckout(CheckoutBase):
554 """Base class for git checkout. Not to be used as-is.""" 557 """Manages a git checkout."""
555 def __init__(self, root_dir, project_name, remote_branch, 558 def __init__(self, root_dir, project_name, remote_branch, git_url,
556 post_processors=None): 559 commit_user, post_processors=None):
557 super(GitCheckoutBase, self).__init__( 560 assert git_url
558 root_dir, project_name, post_processors) 561 assert commit_user
559 # There is no reason to not hardcode it. 562 super(GitCheckout, self).__init__(root_dir, project_name, post_processors)
563 self.git_url = git_url
564 self.commit_user = commit_user
565 self.remote_branch = remote_branch
566 # The working branch where patches will be applied. It will track the
567 # remote branch.
568 self.working_branch = 'working_branch'
569 # There is no reason to not hardcode origin.
560 self.remote = 'origin' 570 self.remote = 'origin'
561 self.remote_branch = remote_branch
562 self.working_branch = 'working_branch'
563 571
564 def prepare(self, revision): 572 def prepare(self, revision):
565 """Resets the git repository in a clean state. 573 """Resets the git repository in a clean state.
566 574
567 Checks it out if not present and deletes the working branch. 575 Checks it out if not present and deletes the working branch.
568 """ 576 """
569 assert self.remote_branch 577 assert self.remote_branch
570 assert os.path.isdir(self.project_path) 578
571 self._check_call_git(['reset', '--hard', '--quiet']) 579 if not os.path.isdir(self.project_path):
580 # Clone the repo if the directory is not present.
581 logging.info('Checking out %s in %s' %
582 (self.project_name, self.project_path))
583 self._check_call_git(
584 ['clone', self.git_url, '-b', self.remote_branch, self.project_path],
585 cwd=None, timeout=FETCH_TIMEOUT)
586 else:
587 # Throw away all uncommitted changes in the existing checkout.
588 self._check_call_git(['checkout', self.remote_branch])
589 self._check_call_git(
590 ['reset', '--hard', '--quiet',
591 '%s/%s' % (self.remote, self.remote_branch)])
592
572 if revision: 593 if revision:
573 try: 594 try:
595 # Look if the commit hash already exist. If so, we can skip a
596 # 'git fetch' call.
574 revision = self._check_output_git(['rev-parse', revision]) 597 revision = self._check_output_git(['rev-parse', revision])
575 except subprocess.CalledProcessError: 598 except subprocess.CalledProcessError:
576 self._check_call_git( 599 self._check_call_git(
577 ['fetch', self.remote, self.remote_branch, '--quiet']) 600 ['fetch', self.remote, self.remote_branch, '--quiet'])
578 revision = self._check_output_git(['rev-parse', revision]) 601 revision = self._check_output_git(['rev-parse', revision])
579 self._check_call_git(['checkout', '--force', '--quiet', revision]) 602 self._check_call_git(['checkout', '--force', '--quiet', revision])
580 else: 603 else:
581 branches, active = self._branches() 604 branches, active = self._branches()
582 if active != 'master': 605 if active != 'master':
583 self._check_call_git(['checkout', '--force', '--quiet', 'master']) 606 self._check_call_git(['checkout', '--force', '--quiet', 'master'])
584 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) 607 self._sync_remote_branch()
608
585 if self.working_branch in branches: 609 if self.working_branch in branches:
586 self._call_git(['branch', '-D', self.working_branch]) 610 self._call_git(['branch', '-D', self.working_branch])
611 return self._get_revision()
587 612
588 def apply_patch(self, patches, post_processors=None, verbose=False): 613 def _sync_remote_branch(self):
589 """Applies a patch on 'working_branch' and switch to it. 614 """Sync the remote branch."""
615 # We do a 'git pull origin master:refs/remotes/origin/master' instead of
616 # 'git pull origin master' because from the manpage for git-pull:
617 # A parameter <ref> without a colon is equivalent to <ref>: when
618 # pulling/fetching, so it merges <ref> into the current branch without
619 # storing the remote branch anywhere locally.
620 self._check_call_git(
621 ['pull', self.remote,
622 '%s:refs/remotes/%s/%s' % (self.remote_branch, self.remote,
M-A Ruel 2013/09/02 20:35:09 I think it'd be more readable if you made it a nam
rmistry 2013/09/03 12:40:36 Done.
623 self.remote_branch),
624 '--quiet'])
625
626 def _get_revision(self):
627 """Gets the current revision from the local branch."""
628 return self._check_output_git(['rev-parse', 'HEAD']).strip()
629
630 def apply_patch(self, patches, post_processors=None, verbose=False,
631 revert=False):
632 """Applies a patch on 'working_branch' and switches to it.
590 633
591 Also commits the changes on the local branch. 634 Also commits the changes on the local branch.
592 635
593 Ignores svn properties and raise an exception on unexpected ones. 636 Ignores svn properties and raise an exception on unexpected ones.
594 """ 637 """
595 post_processors = post_processors or self.post_processors or [] 638 post_processors = post_processors or self.post_processors or []
596 # It this throws, the checkout is corrupted. Maybe worth deleting it and 639 # It this throws, the checkout is corrupted. Maybe worth deleting it and
597 # trying again? 640 # trying again?
598 if self.remote_branch: 641 if self.remote_branch:
599 self._check_call_git( 642 self._check_call_git(
600 ['checkout', '-b', self.working_branch, 643 ['checkout', '-b', self.working_branch, '-t', self.remote_branch,
601 '%s/%s' % (self.remote, self.remote_branch), '--quiet']) 644 '--quiet'])
645
602 for index, p in enumerate(patches): 646 for index, p in enumerate(patches):
603 stdout = [] 647 stdout = []
604 try: 648 try:
605 filepath = os.path.join(self.project_path, p.filename) 649 filepath = os.path.join(self.project_path, p.filename)
606 if p.is_delete: 650 if p.is_delete:
607 if (not os.path.exists(filepath) and 651 if (not os.path.exists(filepath) and
608 any(p1.source_filename == p.filename for p1 in patches[0:index])): 652 any(p1.source_filename == p.filename for p1 in patches[0:index])):
609 # The file was already deleted if a prior patch with file rename 653 # The file was already deleted if a prior patch with file rename
610 # was already processed because 'git apply' did it for us. 654 # was already processed because 'git apply' did it for us.
611 pass 655 pass
(...skipping 12 matching lines...) Expand all
624 f.write(content) 668 f.write(content)
625 stdout.append('Added binary file %d bytes' % len(content)) 669 stdout.append('Added binary file %d bytes' % len(content))
626 cmd = ['add', p.filename] 670 cmd = ['add', p.filename]
627 if verbose: 671 if verbose:
628 cmd.append('--verbose') 672 cmd.append('--verbose')
629 stdout.append(self._check_output_git(cmd)) 673 stdout.append(self._check_output_git(cmd))
630 else: 674 else:
631 # No need to do anything special with p.is_new or if not 675 # No need to do anything special with p.is_new or if not
632 # p.diff_hunks. git apply manages all that already. 676 # p.diff_hunks. git apply manages all that already.
633 cmd = ['apply', '--index', '-p%s' % p.patchlevel] 677 cmd = ['apply', '--index', '-p%s' % p.patchlevel]
678 if revert:
M-A Ruel 2013/09/02 20:35:09 You mean "reversed"? Is that really needed?
rmistry 2013/09/03 12:40:36 Context behind the revert parameters: I am also wo
679 cmd.append('-R')
634 if verbose: 680 if verbose:
635 cmd.append('--verbose') 681 cmd.append('--verbose')
636 stdout.append(self._check_output_git(cmd, stdin=p.get(True))) 682 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
637 for name, value in p.svn_properties: 683 for name, value in p.svn_properties:
638 # Ignore some known auto-props flags through .subversion/config, 684 # Ignore some known auto-props flags through .subversion/config,
639 # bails out on the other ones. 685 # bails out on the other ones.
640 # TODO(maruel): Read ~/.subversion/config and detect the rules that 686 # TODO(maruel): Read ~/.subversion/config and detect the rules that
641 # applies here to figure out if the property will be correctly 687 # applies here to figure out if the property will be correctly
642 # handled. 688 # handled.
643 stdout.append('Property %s=%s' % (name, value)) 689 stdout.append('Property %s=%s' % (name, value))
(...skipping 16 matching lines...) Expand all
660 'While running %s;\n%s%s' % ( 706 'While running %s;\n%s%s' % (
661 ' '.join(e.cmd), 707 ' '.join(e.cmd),
662 align_stdout(stdout), 708 align_stdout(stdout),
663 align_stdout([getattr(e, 'stdout', '')]))) 709 align_stdout([getattr(e, 'stdout', '')])))
664 # Once all the patches are processed and added to the index, commit the 710 # Once all the patches are processed and added to the index, commit the
665 # index. 711 # index.
666 cmd = ['commit', '-m', 'Committed patch'] 712 cmd = ['commit', '-m', 'Committed patch']
667 if verbose: 713 if verbose:
668 cmd.append('--verbose') 714 cmd.append('--verbose')
669 self._check_call_git(cmd) 715 self._check_call_git(cmd)
670 # TODO(maruel): Weirdly enough they don't match, need to investigate. 716 found_files = self._check_output_git(
671 #found_files = self._check_output_git( 717 ['diff', '%s/%s' % (self.remote, self.remote_branch),
672 # ['diff', 'master', '--name-only']).splitlines(False) 718 '--name-only']).splitlines(False)
673 #assert sorted(patches.filenames) == sorted(found_files), ( 719 assert sorted(patches.filenames) == sorted(found_files), (
674 # sorted(out), sorted(found_files)) 720 sorted(patches.filenames), sorted(found_files))
675 721
676 def commit(self, commit_message, user): 722 def commit(self, commit_message, user):
677 """Updates the commit message. 723 """Commits, updates the commit message and pushes."""
724 assert isinstance(commit_message, unicode)
725
726 commit_cmd = ['commit', '--amend', '-m', commit_message]
727 if user and user != self.commit_user:
728 # We do not have the first or last name of the user, grab the username
729 # from the email and call it the original author's name.
730 name = user.split('@')[0]
731 commit_cmd.extend(['--author', '%s <%s>' % (name, user)])
732 self._check_call_git(commit_cmd)
678 733
679 Subclass needs to dcommit or push. 734 # Push to the remote repository.
680 """ 735 self._check_call_git(
681 assert isinstance(commit_message, unicode) 736 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch),
682 self._check_call_git(['commit', '--amend', '-m', commit_message]) 737 '--force', '--quiet'])
683 return self._check_output_git(['rev-parse', 'HEAD']).strip() 738 # Get the revision after the push.
739 revision = self._get_revision()
740 # Switch back to the remote_branch and sync it.
741 self._check_call_git(['checkout', self.remote_branch])
742 self._sync_remote_branch()
743 # Delete the working branch since we are done with it.
744 self._check_call_git(['branch', '-D', self.working_branch])
745
746 return revision
684 747
685 def _check_call_git(self, args, **kwargs): 748 def _check_call_git(self, args, **kwargs):
686 kwargs.setdefault('cwd', self.project_path) 749 kwargs.setdefault('cwd', self.project_path)
687 kwargs.setdefault('stdout', self.VOID) 750 kwargs.setdefault('stdout', self.VOID)
688 kwargs.setdefault('timeout', GLOBAL_TIMEOUT) 751 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
689 return subprocess2.check_call_out(['git'] + args, **kwargs) 752 return subprocess2.check_call_out(['git'] + args, **kwargs)
690 753
691 def _call_git(self, args, **kwargs): 754 def _call_git(self, args, **kwargs):
692 """Like check_call but doesn't throw on failure.""" 755 """Like check_call but doesn't throw on failure."""
693 kwargs.setdefault('cwd', self.project_path) 756 kwargs.setdefault('cwd', self.project_path)
(...skipping 26 matching lines...) Expand all
720 # Revision range is ]rev1, rev2] and ordering matters. 783 # Revision range is ]rev1, rev2] and ordering matters.
721 try: 784 try:
722 out = self._check_output_git( 785 out = self._check_output_git(
723 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)]) 786 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
724 except subprocess.CalledProcessError: 787 except subprocess.CalledProcessError:
725 return None 788 return None
726 return len(out.splitlines()) 789 return len(out.splitlines())
727 790
728 def _fetch_remote(self): 791 def _fetch_remote(self):
729 """Fetches the remote without rebasing.""" 792 """Fetches the remote without rebasing."""
730 raise NotImplementedError() 793 # git fetch is always verbose even with -q, so redirect its output.
731
732
733 class GitCheckout(GitCheckoutBase):
734 """Git checkout implementation."""
735 def _fetch_remote(self):
736 # git fetch is always verbose even with -q -q so redirect its output.
737 self._check_output_git(['fetch', self.remote, self.remote_branch], 794 self._check_output_git(['fetch', self.remote, self.remote_branch],
738 timeout=FETCH_TIMEOUT) 795 timeout=FETCH_TIMEOUT)
739 796
740 797
741 class ReadOnlyCheckout(object): 798 class ReadOnlyCheckout(object):
742 """Converts a checkout into a read-only one.""" 799 """Converts a checkout into a read-only one."""
743 def __init__(self, checkout, post_processors=None): 800 def __init__(self, checkout, post_processors=None):
744 super(ReadOnlyCheckout, self).__init__() 801 super(ReadOnlyCheckout, self).__init__()
745 self.checkout = checkout 802 self.checkout = checkout
746 self.post_processors = (post_processors or []) + ( 803 self.post_processors = (post_processors or []) + (
747 self.checkout.post_processors or []) 804 self.checkout.post_processors or [])
748 805
749 def prepare(self, revision): 806 def prepare(self, revision):
750 return self.checkout.prepare(revision) 807 return self.checkout.prepare(revision)
751 808
752 def get_settings(self, key): 809 def get_settings(self, key):
753 return self.checkout.get_settings(key) 810 return self.checkout.get_settings(key)
754 811
755 def apply_patch(self, patches, post_processors=None, verbose=False): 812 def apply_patch(self, patches, post_processors=None, verbose=False,
813 revert=False):
756 return self.checkout.apply_patch( 814 return self.checkout.apply_patch(
757 patches, post_processors or self.post_processors, verbose) 815 patches, post_processors or self.post_processors, verbose, revert)
758 816
759 def commit(self, message, user): # pylint: disable=R0201 817 def commit(self, message, user): # pylint: disable=R0201
760 logging.info('Would have committed for %s with message: %s' % ( 818 logging.info('Would have committed for %s with message: %s' % (
761 user, message)) 819 user, message))
762 return 'FAKE' 820 return 'FAKE'
763 821
764 def revisions(self, rev1, rev2): 822 def revisions(self, rev1, rev2):
765 return self.checkout.revisions(rev1, rev2) 823 return self.checkout.revisions(rev1, rev2)
766 824
767 @property 825 @property
768 def project_name(self): 826 def project_name(self):
769 return self.checkout.project_name 827 return self.checkout.project_name
770 828
771 @property 829 @property
772 def project_path(self): 830 def project_path(self):
773 return self.checkout.project_path 831 return self.checkout.project_path
OLDNEW
« no previous file with comments | « apply_issue.py ('k') | tests/checkout_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698