OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Gclient-specific SCM-specific operations.""" | 5 """Gclient-specific SCM-specific operations.""" |
6 | 6 |
7 from __future__ import print_function | 7 from __future__ import print_function |
8 | 8 |
9 import errno | 9 import errno |
10 import logging | 10 import logging |
(...skipping 19 matching lines...) Expand all Loading... |
30 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py') | 30 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py') |
31 | 31 |
32 | 32 |
33 class NoUsableRevError(gclient_utils.Error): | 33 class NoUsableRevError(gclient_utils.Error): |
34 """Raised if requested revision isn't found in checkout.""" | 34 """Raised if requested revision isn't found in checkout.""" |
35 | 35 |
36 | 36 |
37 class DiffFiltererWrapper(object): | 37 class DiffFiltererWrapper(object): |
38 """Simple base class which tracks which file is being diffed and | 38 """Simple base class which tracks which file is being diffed and |
39 replaces instances of its file name in the original and | 39 replaces instances of its file name in the original and |
40 working copy lines of the svn/git diff output.""" | 40 working copy lines of the git diff output.""" |
41 index_string = None | 41 index_string = None |
42 original_prefix = "--- " | 42 original_prefix = "--- " |
43 working_prefix = "+++ " | 43 working_prefix = "+++ " |
44 | 44 |
45 def __init__(self, relpath, print_func): | 45 def __init__(self, relpath, print_func): |
46 # Note that we always use '/' as the path separator to be | 46 # Note that we always use '/' as the path separator to be |
47 # consistent with svn's cygwin-style output on Windows | 47 # consistent with cygwin-style output on Windows |
48 self._relpath = relpath.replace("\\", "/") | 48 self._relpath = relpath.replace("\\", "/") |
49 self._current_file = None | 49 self._current_file = None |
50 self._print_func = print_func | 50 self._print_func = print_func |
51 | 51 |
52 def SetCurrentFile(self, current_file): | 52 def SetCurrentFile(self, current_file): |
53 self._current_file = current_file | 53 self._current_file = current_file |
54 | 54 |
55 @property | 55 @property |
56 def _replacement_file(self): | 56 def _replacement_file(self): |
57 return posixpath.join(self._relpath, self._current_file) | 57 return posixpath.join(self._relpath, self._current_file) |
58 | 58 |
59 def _Replace(self, line): | 59 def _Replace(self, line): |
60 return line.replace(self._current_file, self._replacement_file) | 60 return line.replace(self._current_file, self._replacement_file) |
61 | 61 |
62 def Filter(self, line): | 62 def Filter(self, line): |
63 if (line.startswith(self.index_string)): | 63 if (line.startswith(self.index_string)): |
64 self.SetCurrentFile(line[len(self.index_string):]) | 64 self.SetCurrentFile(line[len(self.index_string):]) |
65 line = self._Replace(line) | 65 line = self._Replace(line) |
66 else: | 66 else: |
67 if (line.startswith(self.original_prefix) or | 67 if (line.startswith(self.original_prefix) or |
68 line.startswith(self.working_prefix)): | 68 line.startswith(self.working_prefix)): |
69 line = self._Replace(line) | 69 line = self._Replace(line) |
70 self._print_func(line) | 70 self._print_func(line) |
71 | 71 |
72 | 72 |
73 class SvnDiffFilterer(DiffFiltererWrapper): | |
74 index_string = "Index: " | |
75 | |
76 | |
77 class GitDiffFilterer(DiffFiltererWrapper): | 73 class GitDiffFilterer(DiffFiltererWrapper): |
78 index_string = "diff --git " | 74 index_string = "diff --git " |
79 | 75 |
80 def SetCurrentFile(self, current_file): | 76 def SetCurrentFile(self, current_file): |
81 # Get filename by parsing "a/<filename> b/<filename>" | 77 # Get filename by parsing "a/<filename> b/<filename>" |
82 self._current_file = current_file[:(len(current_file)/2)][2:] | 78 self._current_file = current_file[:(len(current_file)/2)][2:] |
83 | 79 |
84 def _Replace(self, line): | 80 def _Replace(self, line): |
85 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line) | 81 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line) |
86 | 82 |
87 | 83 |
88 ### SCM abstraction layer | 84 ### SCM abstraction layer |
89 | 85 |
90 # Factory Method for SCM wrapper creation | 86 # Factory Method for SCM wrapper creation |
91 | 87 |
92 def GetScmName(url): | 88 def GetScmName(url): |
93 if url: | 89 if not url: |
94 url, _ = gclient_utils.SplitUrlRevision(url) | 90 return None |
95 if (url.startswith('git://') or url.startswith('ssh://') or | 91 url, _ = gclient_utils.SplitUrlRevision(url) |
96 url.startswith('git+http://') or url.startswith('git+https://') or | 92 if url.endswith('.git'): |
97 url.endswith('.git') or url.startswith('sso://') or | 93 return 'git' |
98 'googlesource' in url): | 94 protocol = url.split('://')[0] |
99 return 'git' | 95 if protocol in ( |
100 elif (url.startswith('http://') or url.startswith('https://') or | 96 'file', 'git', 'git+http', 'git+https', 'http', 'https', 'ssh', 'sso'): |
101 url.startswith('svn://') or url.startswith('svn+ssh://')): | 97 return 'git' |
102 return 'svn' | |
103 elif url.startswith('file://'): | |
104 if url.endswith('.git'): | |
105 return 'git' | |
106 return 'svn' | |
107 return None | 98 return None |
108 | 99 |
109 | 100 |
110 def CreateSCM(url, root_dir=None, relpath=None, out_fh=None, out_cb=None): | 101 def CreateSCM(url, root_dir=None, relpath=None, out_fh=None, out_cb=None): |
111 SCM_MAP = { | 102 SCM_MAP = { |
112 'svn' : SVNWrapper, | |
113 'git' : GitWrapper, | 103 'git' : GitWrapper, |
114 } | 104 } |
115 | 105 |
116 scm_name = GetScmName(url) | 106 scm_name = GetScmName(url) |
117 if not scm_name in SCM_MAP: | 107 if not scm_name in SCM_MAP: |
118 raise gclient_utils.Error('No SCM found for url %s' % url) | 108 raise gclient_utils.Error('No SCM found for url %s' % url) |
119 scm_class = SCM_MAP[scm_name] | 109 scm_class = SCM_MAP[scm_name] |
120 if not scm_class.BinaryExists(): | 110 if not scm_class.BinaryExists(): |
121 raise gclient_utils.Error('%s command not found' % scm_name) | 111 raise gclient_utils.Error('%s command not found' % scm_name) |
122 return scm_class(url, root_dir, relpath, out_fh, out_cb) | 112 return scm_class(url, root_dir, relpath, out_fh, out_cb) |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
185 if os.path.exists(os.path.join(self.checkout_path, '.git')): | 175 if os.path.exists(os.path.join(self.checkout_path, '.git')): |
186 actual_remote_url = self._get_first_remote_url(self.checkout_path) | 176 actual_remote_url = self._get_first_remote_url(self.checkout_path) |
187 | 177 |
188 mirror = self.GetCacheMirror() | 178 mirror = self.GetCacheMirror() |
189 # If the cache is used, obtain the actual remote URL from there. | 179 # If the cache is used, obtain the actual remote URL from there. |
190 if (mirror and mirror.exists() and | 180 if (mirror and mirror.exists() and |
191 mirror.mirror_path.replace('\\', '/') == | 181 mirror.mirror_path.replace('\\', '/') == |
192 actual_remote_url.replace('\\', '/')): | 182 actual_remote_url.replace('\\', '/')): |
193 actual_remote_url = self._get_first_remote_url(mirror.mirror_path) | 183 actual_remote_url = self._get_first_remote_url(mirror.mirror_path) |
194 return actual_remote_url | 184 return actual_remote_url |
195 | |
196 # Svn | |
197 if os.path.exists(os.path.join(self.checkout_path, '.svn')): | |
198 return scm.SVN.CaptureLocalInfo([], self.checkout_path)['URL'] | |
199 return None | 185 return None |
200 | 186 |
201 def DoesRemoteURLMatch(self, options): | 187 def DoesRemoteURLMatch(self, options): |
202 """Determine whether the remote URL of this checkout is the expected URL.""" | 188 """Determine whether the remote URL of this checkout is the expected URL.""" |
203 if not os.path.exists(self.checkout_path): | 189 if not os.path.exists(self.checkout_path): |
204 # A checkout which doesn't exist can't be broken. | 190 # A checkout which doesn't exist can't be broken. |
205 return True | 191 return True |
206 | 192 |
207 actual_remote_url = self.GetActualRemoteURL(options) | 193 actual_remote_url = self.GetActualRemoteURL(options) |
208 if actual_remote_url: | 194 if actual_remote_url: |
209 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/') | 195 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/') |
210 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/')) | 196 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/')) |
211 else: | 197 else: |
212 # This may occur if the self.checkout_path exists but does not contain a | 198 # This may occur if the self.checkout_path exists but does not contain a |
213 # valid git or svn checkout. | 199 # valid git checkout. |
214 return False | 200 return False |
215 | 201 |
216 def _DeleteOrMove(self, force): | 202 def _DeleteOrMove(self, force): |
217 """Delete the checkout directory or move it out of the way. | 203 """Delete the checkout directory or move it out of the way. |
218 | 204 |
219 Args: | 205 Args: |
220 force: bool; if True, delete the directory. Otherwise, just move it. | 206 force: bool; if True, delete the directory. Otherwise, just move it. |
221 """ | 207 """ |
222 if force and os.environ.get('CHROME_HEADLESS') == '1': | 208 if force and os.environ.get('CHROME_HEADLESS') == '1': |
223 self.Print('_____ Conflicting directory found in %s. Removing.' | 209 self.Print('_____ Conflicting directory found in %s. Removing.' |
(...skipping 272 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
496 | 482 |
497 if return_early: | 483 if return_early: |
498 return self._Capture(['rev-parse', '--verify', 'HEAD']) | 484 return self._Capture(['rev-parse', '--verify', 'HEAD']) |
499 | 485 |
500 cur_branch = self._GetCurrentBranch() | 486 cur_branch = self._GetCurrentBranch() |
501 | 487 |
502 # Cases: | 488 # Cases: |
503 # 0) HEAD is detached. Probably from our initial clone. | 489 # 0) HEAD is detached. Probably from our initial clone. |
504 # - make sure HEAD is contained by a named ref, then update. | 490 # - make sure HEAD is contained by a named ref, then update. |
505 # Cases 1-4. HEAD is a branch. | 491 # Cases 1-4. HEAD is a branch. |
506 # 1) current branch is not tracking a remote branch (could be git-svn) | 492 # 1) current branch is not tracking a remote branch |
507 # - try to rebase onto the new hash or branch | 493 # - try to rebase onto the new hash or branch |
508 # 2) current branch is tracking a remote branch with local committed | 494 # 2) current branch is tracking a remote branch with local committed |
509 # changes, but the DEPS file switched to point to a hash | 495 # changes, but the DEPS file switched to point to a hash |
510 # - rebase those changes on top of the hash | 496 # - rebase those changes on top of the hash |
511 # 3) current branch is tracking a remote branch w/or w/out changes, and | 497 # 3) current branch is tracking a remote branch w/or w/out changes, and |
512 # no DEPS switch | 498 # no DEPS switch |
513 # - see if we can FF, if not, prompt the user for rebase, merge, or stop | 499 # - see if we can FF, if not, prompt the user for rebase, merge, or stop |
514 # 4) current branch is tracking a remote branch, but DEPS switches to a | 500 # 4) current branch is tracking a remote branch, but DEPS switches to a |
515 # different remote branch, and | 501 # different remote branch, and |
516 # a) current branch has no local changes, and --force: | 502 # a) current branch has no local changes, and --force: |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
566 self._Checkout( | 552 self._Checkout( |
567 options, | 553 options, |
568 revision, | 554 revision, |
569 force=(options.force and options.delete_unversioned_trees), | 555 force=(options.force and options.delete_unversioned_trees), |
570 quiet=True, | 556 quiet=True, |
571 ) | 557 ) |
572 if not printed_path: | 558 if not printed_path: |
573 self.Print('_____ %s%s' % (self.relpath, rev_str), timestamp=False) | 559 self.Print('_____ %s%s' % (self.relpath, rev_str), timestamp=False) |
574 elif current_type == 'hash': | 560 elif current_type == 'hash': |
575 # case 1 | 561 # case 1 |
576 if scm.GIT.IsGitSvn(self.checkout_path) and upstream_branch is not None: | 562 # Can't find a merge-base since we don't know our upstream. That makes |
577 # Our git-svn branch (upstream_branch) is our upstream | 563 # this command VERY likely to produce a rebase failure. For now we |
578 self._AttemptRebase(upstream_branch, files, options, | 564 # assume origin is our upstream since that's what the old behavior was. |
579 newbase=revision, printed_path=printed_path, | 565 upstream_branch = self.remote |
580 merge=options.merge) | 566 if options.revision or deps_revision: |
581 printed_path = True | 567 upstream_branch = revision |
582 else: | 568 self._AttemptRebase(upstream_branch, files, options, |
583 # Can't find a merge-base since we don't know our upstream. That makes | 569 printed_path=printed_path, merge=options.merge) |
584 # this command VERY likely to produce a rebase failure. For now we | 570 printed_path = True |
585 # assume origin is our upstream since that's what the old behavior was. | |
586 upstream_branch = self.remote | |
587 if options.revision or deps_revision: | |
588 upstream_branch = revision | |
589 self._AttemptRebase(upstream_branch, files, options, | |
590 printed_path=printed_path, merge=options.merge) | |
591 printed_path = True | |
592 elif rev_type == 'hash': | 571 elif rev_type == 'hash': |
593 # case 2 | 572 # case 2 |
594 self._AttemptRebase(upstream_branch, files, options, | 573 self._AttemptRebase(upstream_branch, files, options, |
595 newbase=revision, printed_path=printed_path, | 574 newbase=revision, printed_path=printed_path, |
596 merge=options.merge) | 575 merge=options.merge) |
597 printed_path = True | 576 printed_path = True |
598 elif remote_ref and ''.join(remote_ref) != upstream_branch: | 577 elif remote_ref and ''.join(remote_ref) != upstream_branch: |
599 # case 4 | 578 # case 4 |
600 new_base = ''.join(remote_ref) | 579 new_base = ''.join(remote_ref) |
601 if not printed_path: | 580 if not printed_path: |
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
781 """Display status information.""" | 760 """Display status information.""" |
782 if not os.path.isdir(self.checkout_path): | 761 if not os.path.isdir(self.checkout_path): |
783 self.Print('________ couldn\'t run status in %s:\n' | 762 self.Print('________ couldn\'t run status in %s:\n' |
784 'The directory does not exist.' % self.checkout_path) | 763 'The directory does not exist.' % self.checkout_path) |
785 else: | 764 else: |
786 try: | 765 try: |
787 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])] | 766 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])] |
788 except subprocess2.CalledProcessError: | 767 except subprocess2.CalledProcessError: |
789 merge_base = [] | 768 merge_base = [] |
790 self._Run(['diff', '--name-status'] + merge_base, options, | 769 self._Run(['diff', '--name-status'] + merge_base, options, |
791 stdout=self.out_fh) | 770 stdout=self.out_fh, always=options.verbose) |
792 if file_list is not None: | 771 if file_list is not None: |
793 files = self._Capture(['diff', '--name-only'] + merge_base).split() | 772 files = self._Capture(['diff', '--name-only'] + merge_base).split() |
794 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) | 773 file_list.extend([os.path.join(self.checkout_path, f) for f in files]) |
795 | 774 |
796 def GetUsableRev(self, rev, options): | 775 def GetUsableRev(self, rev, options): |
797 """Finds a useful revision for this repository. | 776 """Finds a useful revision for this repository.""" |
798 | |
799 If SCM is git-svn and the head revision is less than |rev|, git svn fetch | |
800 will be called on the source.""" | |
801 sha1 = None | 777 sha1 = None |
802 if not os.path.isdir(self.checkout_path): | 778 if not os.path.isdir(self.checkout_path): |
803 raise NoUsableRevError( | 779 raise NoUsableRevError( |
804 ( 'We could not find a valid hash for safesync_url response "%s".\n' | 780 ( 'We could not find a valid hash for safesync_url response "%s".\n' |
805 'Safesync URLs with a git checkout currently require the repo to\n' | 781 'Safesync URLs with a git checkout currently require the repo to\n' |
806 'be cloned without a safesync_url before adding the safesync_url.\n' | 782 'be cloned without a safesync_url before adding the safesync_url.\n' |
807 'For more info, see: ' | 783 'For more info, see: ' |
808 'http://code.google.com/p/chromium/wiki/UsingNewGit' | 784 'http://code.google.com/p/chromium/wiki/UsingNewGit' |
809 '#Initial_checkout' ) % rev) | 785 '#Initial_checkout' ) % rev) |
810 elif rev.isdigit() and len(rev) < 7: | 786 |
811 # Handles an SVN rev. As an optimization, only verify an SVN revision as | 787 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev): |
812 # [0-9]{1,6} for now to avoid making a network request. | 788 sha1 = rev |
813 if scm.GIT.IsGitSvn(cwd=self.checkout_path): | |
814 local_head = scm.GIT.GetGitSvnHeadRev(cwd=self.checkout_path) | |
815 if not local_head or local_head < int(rev): | |
816 try: | |
817 logging.debug('Looking for git-svn configuration optimizations.') | |
818 if scm.GIT.Capture(['config', '--get', 'svn-remote.svn.fetch'], | |
819 cwd=self.checkout_path): | |
820 self._Fetch(options) | |
821 except subprocess2.CalledProcessError: | |
822 logging.debug('git config --get svn-remote.svn.fetch failed, ' | |
823 'ignoring possible optimization.') | |
824 if options.verbose: | |
825 self.Print('Running git svn fetch. This might take a while.\n') | |
826 scm.GIT.Capture(['svn', 'fetch'], cwd=self.checkout_path) | |
827 try: | |
828 sha1 = scm.GIT.GetBlessedSha1ForSvnRev( | |
829 cwd=self.checkout_path, rev=rev) | |
830 except gclient_utils.Error, e: | |
831 sha1 = e.message | |
832 self.Print('Warning: Could not find a git revision with accurate\n' | |
833 '.DEPS.git that maps to SVN revision %s. Sync-ing to\n' | |
834 'the closest sane git revision, which is:\n' | |
835 ' %s\n' % (rev, e.message)) | |
836 if not sha1: | |
837 raise NoUsableRevError( | |
838 ( 'It appears that either your git-svn remote is incorrectly\n' | |
839 'configured or the revision in your safesync_url is\n' | |
840 'higher than git-svn remote\'s HEAD as we couldn\'t find a\n' | |
841 'corresponding git hash for SVN rev %s.' ) % rev) | |
842 else: | 789 else: |
| 790 # May exist in origin, but we don't have it yet, so fetch and look |
| 791 # again. |
| 792 self._Fetch(options) |
843 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev): | 793 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev): |
844 sha1 = rev | 794 sha1 = rev |
845 else: | |
846 # May exist in origin, but we don't have it yet, so fetch and look | |
847 # again. | |
848 self._Fetch(options) | |
849 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev): | |
850 sha1 = rev | |
851 | 795 |
852 if not sha1: | 796 if not sha1: |
853 raise NoUsableRevError( | 797 raise NoUsableRevError( |
854 ( 'We could not find a valid hash for safesync_url response "%s".\n' | 798 ('We could not find a valid hash for safesync_url response "%s".\n' |
855 'Safesync URLs with a git checkout currently require a git-svn\n' | 799 'Please ensure that your safesync_url provides git sha1 hashes.\n' |
856 'remote or a safesync_url that provides git sha1s. Please add a\n' | 800 'For more info, see:\n' |
857 'git-svn remote or change your safesync_url. For more info, see:\n' | 801 'http://code.google.com/p/chromium/wiki/UsingNewGit#Initial_checkout' |
858 'http://code.google.com/p/chromium/wiki/UsingNewGit' | 802 ) % rev) |
859 '#Initial_checkout' ) % rev) | |
860 | 803 |
861 return sha1 | 804 return sha1 |
862 | 805 |
863 def FullUrlForRelativeUrl(self, url): | 806 def FullUrlForRelativeUrl(self, url): |
864 # Strip from last '/' | 807 # Strip from last '/' |
865 # Equivalent to unix basename | 808 # Equivalent to unix basename |
866 base_url = self.url | 809 base_url = self.url |
867 return base_url[:base_url.rfind('/')] + url | 810 return base_url[:base_url.rfind('/')] + url |
868 | 811 |
869 def GetGitBackupDirPath(self): | 812 def GetGitBackupDirPath(self): |
(...skipping 362 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1232 kwargs.setdefault('cwd', self.checkout_path) | 1175 kwargs.setdefault('cwd', self.checkout_path) |
1233 kwargs.setdefault('stdout', self.out_fh) | 1176 kwargs.setdefault('stdout', self.out_fh) |
1234 kwargs['filter_fn'] = self.filter | 1177 kwargs['filter_fn'] = self.filter |
1235 kwargs.setdefault('print_stdout', False) | 1178 kwargs.setdefault('print_stdout', False) |
1236 env = scm.GIT.ApplyEnvVars(kwargs) | 1179 env = scm.GIT.ApplyEnvVars(kwargs) |
1237 cmd = ['git'] + args | 1180 cmd = ['git'] + args |
1238 if show_header: | 1181 if show_header: |
1239 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs) | 1182 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs) |
1240 else: | 1183 else: |
1241 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs) | 1184 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs) |
1242 | |
1243 | |
1244 class SVNWrapper(SCMWrapper): | |
1245 """ Wrapper for SVN """ | |
1246 name = 'svn' | |
1247 _PRINTED_DEPRECATION = False | |
1248 | |
1249 _MESSAGE = ( | |
1250 'Oh hai! You are using subversion. Chrome infra is eager to get rid of', | |
1251 'svn support so please switch to git.', | |
1252 'Tracking bug: http://crbug.com/475320', | |
1253 'If you are a project owner, you may request git migration assistance at: ', | |
1254 ' https://code.google.com/p/chromium/issues/entry?template=Infra-Git') | |
1255 | |
1256 def __init__(self, *args, **kwargs): | |
1257 super(SVNWrapper, self).__init__(*args, **kwargs) | |
1258 suppress_deprecated_notice = os.environ.get( | |
1259 'SUPPRESS_DEPRECATED_SVN_NOTICE', False) | |
1260 if not SVNWrapper._PRINTED_DEPRECATION and not suppress_deprecated_notice: | |
1261 SVNWrapper._PRINTED_DEPRECATION = True | |
1262 sys.stderr.write('\n'.join(self._MESSAGE) + '\n') | |
1263 | |
1264 @staticmethod | |
1265 def BinaryExists(): | |
1266 """Returns true if the command exists.""" | |
1267 try: | |
1268 result, version = scm.SVN.AssertVersion('1.4') | |
1269 if not result: | |
1270 raise gclient_utils.Error('SVN version is older than 1.4: %s' % version) | |
1271 return result | |
1272 except OSError: | |
1273 return False | |
1274 | |
1275 def GetCheckoutRoot(self): | |
1276 return scm.SVN.GetCheckoutRoot(self.checkout_path) | |
1277 | |
1278 def GetRevisionDate(self, revision): | |
1279 """Returns the given revision's date in ISO-8601 format (which contains the | |
1280 time zone).""" | |
1281 date = scm.SVN.Capture( | |
1282 ['propget', '--revprop', 'svn:date', '-r', revision], | |
1283 os.path.join(self.checkout_path, '.')) | |
1284 return date.strip() | |
1285 | |
1286 def cleanup(self, options, args, _file_list): | |
1287 """Cleanup working copy.""" | |
1288 self._Run(['cleanup'] + args, options) | |
1289 | |
1290 def diff(self, options, args, _file_list): | |
1291 # NOTE: This function does not currently modify file_list. | |
1292 if not os.path.isdir(self.checkout_path): | |
1293 raise gclient_utils.Error('Directory %s is not present.' % | |
1294 self.checkout_path) | |
1295 self._Run(['diff'] + args, options) | |
1296 | |
1297 def pack(self, _options, args, _file_list): | |
1298 """Generates a patch file which can be applied to the root of the | |
1299 repository.""" | |
1300 if not os.path.isdir(self.checkout_path): | |
1301 raise gclient_utils.Error('Directory %s is not present.' % | |
1302 self.checkout_path) | |
1303 gclient_utils.CheckCallAndFilter( | |
1304 ['svn', 'diff', '-x', '--ignore-eol-style'] + args, | |
1305 cwd=self.checkout_path, | |
1306 print_stdout=False, | |
1307 filter_fn=SvnDiffFilterer(self.relpath, print_func=self.Print).Filter) | |
1308 | |
1309 def update(self, options, args, file_list): | |
1310 """Runs svn to update or transparently checkout the working copy. | |
1311 | |
1312 All updated files will be appended to file_list. | |
1313 | |
1314 Raises: | |
1315 Error: if can't get URL for relative path. | |
1316 """ | |
1317 # Only update if hg is not controlling the directory. | |
1318 hg_path = os.path.join(self.checkout_path, '.hg') | |
1319 if os.path.exists(hg_path): | |
1320 self.Print('________ found .hg directory; skipping %s' % self.relpath) | |
1321 return | |
1322 | |
1323 if args: | |
1324 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args)) | |
1325 | |
1326 # revision is the revision to match. It is None if no revision is specified, | |
1327 # i.e. the 'deps ain't pinned'. | |
1328 url, revision = gclient_utils.SplitUrlRevision(self.url) | |
1329 # Keep the original unpinned url for reference in case the repo is switched. | |
1330 base_url = url | |
1331 managed = True | |
1332 if options.revision: | |
1333 # Override the revision number. | |
1334 revision = str(options.revision) | |
1335 if revision: | |
1336 if revision != 'unmanaged': | |
1337 forced_revision = True | |
1338 # Reconstruct the url. | |
1339 url = '%s@%s' % (url, revision) | |
1340 rev_str = ' at %s' % revision | |
1341 else: | |
1342 managed = False | |
1343 revision = None | |
1344 else: | |
1345 forced_revision = False | |
1346 rev_str = '' | |
1347 | |
1348 exists = os.path.exists(self.checkout_path) | |
1349 if exists and managed: | |
1350 # Git is only okay if it's a git-svn checkout of the right repo. | |
1351 if scm.GIT.IsGitSvn(self.checkout_path): | |
1352 remote_url = scm.GIT.Capture(['config', '--local', '--get', | |
1353 'svn-remote.svn.url'], | |
1354 cwd=self.checkout_path).rstrip() | |
1355 if remote_url.rstrip('/') == base_url.rstrip('/'): | |
1356 self.Print('\n_____ %s looks like a git-svn checkout. Skipping.' | |
1357 % self.relpath) | |
1358 return # TODO(borenet): Get the svn revision number? | |
1359 | |
1360 # Get the existing scm url and the revision number of the current checkout. | |
1361 if exists and managed: | |
1362 try: | |
1363 from_info = scm.SVN.CaptureLocalInfo( | |
1364 [], os.path.join(self.checkout_path, '.')) | |
1365 except (gclient_utils.Error, subprocess2.CalledProcessError): | |
1366 self._DeleteOrMove(options.force) | |
1367 exists = False | |
1368 | |
1369 BASE_URLS = { | |
1370 '/chrome/trunk/src': 'gs://chromium-svn-checkout/chrome/', | |
1371 '/blink/trunk': 'gs://chromium-svn-checkout/blink/', | |
1372 } | |
1373 WHITELISTED_ROOTS = [ | |
1374 'svn://svn.chromium.org', | |
1375 'svn://svn-mirror.golo.chromium.org', | |
1376 ] | |
1377 if not exists: | |
1378 try: | |
1379 # Split out the revision number since it's not useful for us. | |
1380 base_path = urlparse.urlparse(url).path.split('@')[0] | |
1381 # Check to see if we're on a whitelisted root. We do this because | |
1382 # only some svn servers have matching UUIDs. | |
1383 local_parsed = urlparse.urlparse(url) | |
1384 local_root = '%s://%s' % (local_parsed.scheme, local_parsed.netloc) | |
1385 if ('CHROME_HEADLESS' in os.environ | |
1386 and sys.platform == 'linux2' # TODO(hinoka): Enable for win/mac. | |
1387 and base_path in BASE_URLS | |
1388 and local_root in WHITELISTED_ROOTS): | |
1389 | |
1390 # Use a tarball for initial sync if we are on a bot. | |
1391 # Get an unauthenticated gsutil instance. | |
1392 gsutil = download_from_google_storage.Gsutil( | |
1393 GSUTIL_DEFAULT_PATH, boto_path=os.devnull) | |
1394 | |
1395 gs_path = BASE_URLS[base_path] | |
1396 _, out, _ = gsutil.check_call('ls', gs_path) | |
1397 # So that we can get the most recent revision. | |
1398 sorted_items = sorted(out.splitlines()) | |
1399 latest_checkout = sorted_items[-1] | |
1400 | |
1401 tempdir = tempfile.mkdtemp() | |
1402 self.Print('Downloading %s...' % latest_checkout) | |
1403 code, out, err = gsutil.check_call('cp', latest_checkout, tempdir) | |
1404 if code: | |
1405 self.Print('%s\n%s' % (out, err)) | |
1406 raise Exception() | |
1407 filename = latest_checkout.split('/')[-1] | |
1408 tarball = os.path.join(tempdir, filename) | |
1409 self.Print('Unpacking into %s...' % self.checkout_path) | |
1410 gclient_utils.safe_makedirs(self.checkout_path) | |
1411 # TODO(hinoka): Use 7z for windows. | |
1412 cmd = ['tar', '--extract', '--ungzip', | |
1413 '--directory', self.checkout_path, | |
1414 '--file', tarball] | |
1415 gclient_utils.CheckCallAndFilter( | |
1416 cmd, stdout=sys.stdout, print_stdout=True) | |
1417 | |
1418 self.Print('Deleting temp file') | |
1419 gclient_utils.rmtree(tempdir) | |
1420 | |
1421 # Rewrite the repository root to match. | |
1422 tarball_url = scm.SVN.CaptureLocalInfo( | |
1423 ['.'], self.checkout_path)['Repository Root'] | |
1424 tarball_parsed = urlparse.urlparse(tarball_url) | |
1425 tarball_root = '%s://%s' % (tarball_parsed.scheme, | |
1426 tarball_parsed.netloc) | |
1427 | |
1428 if tarball_root != local_root: | |
1429 self.Print('Switching repository root to %s' % local_root) | |
1430 self._Run(['switch', '--relocate', tarball_root, | |
1431 local_root, self.checkout_path], | |
1432 options) | |
1433 except Exception as e: | |
1434 self.Print('We tried to get a source tarball but failed.') | |
1435 self.Print('Resuming normal operations.') | |
1436 self.Print(str(e)) | |
1437 | |
1438 gclient_utils.safe_makedirs(os.path.dirname(self.checkout_path)) | |
1439 # We need to checkout. | |
1440 command = ['checkout', url, self.checkout_path] | |
1441 command = self._AddAdditionalUpdateFlags(command, options, revision) | |
1442 self._RunAndGetFileList(command, options, file_list, self._root_dir) | |
1443 return self.Svnversion() | |
1444 | |
1445 if not managed: | |
1446 self.Print(('________ unmanaged solution; skipping %s' % self.relpath)) | |
1447 if os.path.exists(os.path.join(self.checkout_path, '.svn')): | |
1448 return self.Svnversion() | |
1449 return | |
1450 | |
1451 if 'URL' not in from_info: | |
1452 raise gclient_utils.Error( | |
1453 ('gclient is confused. Couldn\'t get the url for %s.\n' | |
1454 'Try using @unmanaged.\n%s') % ( | |
1455 self.checkout_path, from_info)) | |
1456 | |
1457 # Look for locked directories. | |
1458 dir_info = scm.SVN.CaptureStatus( | |
1459 None, os.path.join(self.checkout_path, '.')) | |
1460 if any(d[0][2] == 'L' for d in dir_info): | |
1461 try: | |
1462 self._Run(['cleanup', self.checkout_path], options) | |
1463 except subprocess2.CalledProcessError, e: | |
1464 # Get the status again, svn cleanup may have cleaned up at least | |
1465 # something. | |
1466 dir_info = scm.SVN.CaptureStatus( | |
1467 None, os.path.join(self.checkout_path, '.')) | |
1468 | |
1469 # Try to fix the failures by removing troublesome files. | |
1470 for d in dir_info: | |
1471 if d[0][2] == 'L': | |
1472 if d[0][0] == '!' and options.force: | |
1473 # We don't pass any files/directories to CaptureStatus and set | |
1474 # cwd=self.checkout_path, so we should get relative paths here. | |
1475 assert not os.path.isabs(d[1]) | |
1476 path_to_remove = os.path.normpath( | |
1477 os.path.join(self.checkout_path, d[1])) | |
1478 self.Print('Removing troublesome path %s' % path_to_remove) | |
1479 gclient_utils.rmtree(path_to_remove) | |
1480 else: | |
1481 self.Print( | |
1482 'Not removing troublesome path %s automatically.' % d[1]) | |
1483 if d[0][0] == '!': | |
1484 self.Print('You can pass --force to enable automatic removal.') | |
1485 raise e | |
1486 | |
1487 if from_info['URL'].rstrip('/') != base_url.rstrip('/'): | |
1488 # The repository url changed, need to switch. | |
1489 try: | |
1490 to_info = scm.SVN.CaptureRemoteInfo(url) | |
1491 except (gclient_utils.Error, subprocess2.CalledProcessError): | |
1492 # The url is invalid or the server is not accessible, it's safer to bail | |
1493 # out right now. | |
1494 raise gclient_utils.Error('This url is unreachable: %s' % url) | |
1495 can_switch = ((from_info['Repository Root'] != to_info['Repository Root']) | |
1496 and (from_info['UUID'] == to_info['UUID'])) | |
1497 if can_switch: | |
1498 self.Print('_____ relocating %s to a new checkout' % self.relpath) | |
1499 # We have different roots, so check if we can switch --relocate. | |
1500 # Subversion only permits this if the repository UUIDs match. | |
1501 # Perform the switch --relocate, then rewrite the from_url | |
1502 # to reflect where we "are now." (This is the same way that | |
1503 # Subversion itself handles the metadata when switch --relocate | |
1504 # is used.) This makes the checks below for whether we | |
1505 # can update to a revision or have to switch to a different | |
1506 # branch work as expected. | |
1507 # TODO(maruel): TEST ME ! | |
1508 command = ['switch', '--relocate', | |
1509 from_info['Repository Root'], | |
1510 to_info['Repository Root'], | |
1511 self.relpath] | |
1512 self._Run(command, options, cwd=self._root_dir) | |
1513 from_info['URL'] = from_info['URL'].replace( | |
1514 from_info['Repository Root'], | |
1515 to_info['Repository Root']) | |
1516 else: | |
1517 if not options.force and not options.reset: | |
1518 # Look for local modifications but ignore unversioned files. | |
1519 for status in scm.SVN.CaptureStatus(None, self.checkout_path): | |
1520 if status[0][0] != '?': | |
1521 raise gclient_utils.Error( | |
1522 ('Can\'t switch the checkout to %s; UUID don\'t match and ' | |
1523 'there is local changes in %s. Delete the directory and ' | |
1524 'try again.') % (url, self.checkout_path)) | |
1525 # Ok delete it. | |
1526 self.Print('_____ switching %s to a new checkout' % self.relpath) | |
1527 gclient_utils.rmtree(self.checkout_path) | |
1528 # We need to checkout. | |
1529 command = ['checkout', url, self.checkout_path] | |
1530 command = self._AddAdditionalUpdateFlags(command, options, revision) | |
1531 self._RunAndGetFileList(command, options, file_list, self._root_dir) | |
1532 return self.Svnversion() | |
1533 | |
1534 # If the provided url has a revision number that matches the revision | |
1535 # number of the existing directory, then we don't need to bother updating. | |
1536 if not options.force and str(from_info['Revision']) == revision: | |
1537 if options.verbose or not forced_revision: | |
1538 self.Print('_____ %s%s' % (self.relpath, rev_str), timestamp=False) | |
1539 else: | |
1540 command = ['update', self.checkout_path] | |
1541 command = self._AddAdditionalUpdateFlags(command, options, revision) | |
1542 self._RunAndGetFileList(command, options, file_list, self._root_dir) | |
1543 | |
1544 # If --reset and --delete_unversioned_trees are specified, remove any | |
1545 # untracked files and directories. | |
1546 if options.reset and options.delete_unversioned_trees: | |
1547 for status in scm.SVN.CaptureStatus(None, self.checkout_path): | |
1548 full_path = os.path.join(self.checkout_path, status[1]) | |
1549 if (status[0][0] == '?' | |
1550 and os.path.isdir(full_path) | |
1551 and not os.path.islink(full_path)): | |
1552 self.Print('_____ removing unversioned directory %s' % status[1]) | |
1553 gclient_utils.rmtree(full_path) | |
1554 return self.Svnversion() | |
1555 | |
1556 def updatesingle(self, options, args, file_list): | |
1557 filename = args.pop() | |
1558 if scm.SVN.AssertVersion("1.5")[0]: | |
1559 if not os.path.exists(os.path.join(self.checkout_path, '.svn')): | |
1560 # Create an empty checkout and then update the one file we want. Future | |
1561 # operations will only apply to the one file we checked out. | |
1562 command = ["checkout", "--depth", "empty", self.url, self.checkout_path] | |
1563 self._Run(command, options, cwd=self._root_dir) | |
1564 if os.path.exists(os.path.join(self.checkout_path, filename)): | |
1565 os.remove(os.path.join(self.checkout_path, filename)) | |
1566 command = ["update", filename] | |
1567 self._RunAndGetFileList(command, options, file_list) | |
1568 # After the initial checkout, we can use update as if it were any other | |
1569 # dep. | |
1570 self.update(options, args, file_list) | |
1571 else: | |
1572 # If the installed version of SVN doesn't support --depth, fallback to | |
1573 # just exporting the file. This has the downside that revision | |
1574 # information is not stored next to the file, so we will have to | |
1575 # re-export the file every time we sync. | |
1576 if not os.path.exists(self.checkout_path): | |
1577 gclient_utils.safe_makedirs(self.checkout_path) | |
1578 command = ["export", os.path.join(self.url, filename), | |
1579 os.path.join(self.checkout_path, filename)] | |
1580 command = self._AddAdditionalUpdateFlags(command, options, | |
1581 options.revision) | |
1582 self._Run(command, options, cwd=self._root_dir) | |
1583 | |
1584 def revert(self, options, _args, file_list): | |
1585 """Reverts local modifications. Subversion specific. | |
1586 | |
1587 All reverted files will be appended to file_list, even if Subversion | |
1588 doesn't know about them. | |
1589 """ | |
1590 if not os.path.isdir(self.checkout_path): | |
1591 if os.path.exists(self.checkout_path): | |
1592 gclient_utils.rmtree(self.checkout_path) | |
1593 # svn revert won't work if the directory doesn't exist. It needs to | |
1594 # checkout instead. | |
1595 self.Print('_____ %s is missing, synching instead' % self.relpath) | |
1596 # Don't reuse the args. | |
1597 return self.update(options, [], file_list) | |
1598 | |
1599 if not os.path.isdir(os.path.join(self.checkout_path, '.svn')): | |
1600 if os.path.isdir(os.path.join(self.checkout_path, '.git')): | |
1601 self.Print('________ found .git directory; skipping %s' % self.relpath) | |
1602 return | |
1603 if os.path.isdir(os.path.join(self.checkout_path, '.hg')): | |
1604 self.Print('________ found .hg directory; skipping %s' % self.relpath) | |
1605 return | |
1606 if not options.force: | |
1607 raise gclient_utils.Error('Invalid checkout path, aborting') | |
1608 self.Print( | |
1609 '\n_____ %s is not a valid svn checkout, synching instead' % | |
1610 self.relpath) | |
1611 gclient_utils.rmtree(self.checkout_path) | |
1612 # Don't reuse the args. | |
1613 return self.update(options, [], file_list) | |
1614 | |
1615 def printcb(file_status): | |
1616 if file_list is not None: | |
1617 file_list.append(file_status[1]) | |
1618 if logging.getLogger().isEnabledFor(logging.INFO): | |
1619 logging.info('%s%s' % (file_status[0], file_status[1])) | |
1620 else: | |
1621 self.Print(os.path.join(self.checkout_path, file_status[1])) | |
1622 scm.SVN.Revert(self.checkout_path, callback=printcb) | |
1623 | |
1624 # Revert() may delete the directory altogether. | |
1625 if not os.path.isdir(self.checkout_path): | |
1626 # Don't reuse the args. | |
1627 return self.update(options, [], file_list) | |
1628 | |
1629 try: | |
1630 # svn revert is so broken we don't even use it. Using | |
1631 # "svn up --revision BASE" achieve the same effect. | |
1632 # file_list will contain duplicates. | |
1633 self._RunAndGetFileList(['update', '--revision', 'BASE'], options, | |
1634 file_list) | |
1635 except OSError, e: | |
1636 # Maybe the directory disappeared meanwhile. Do not throw an exception. | |
1637 logging.error('Failed to update:\n%s' % str(e)) | |
1638 | |
1639 def revinfo(self, _options, _args, _file_list): | |
1640 """Display revision""" | |
1641 try: | |
1642 return scm.SVN.CaptureRevision(self.checkout_path) | |
1643 except (gclient_utils.Error, subprocess2.CalledProcessError): | |
1644 return None | |
1645 | |
1646 def runhooks(self, options, args, file_list): | |
1647 self.status(options, args, file_list) | |
1648 | |
1649 def status(self, options, args, file_list): | |
1650 """Display status information.""" | |
1651 command = ['status'] + args | |
1652 if not os.path.isdir(self.checkout_path): | |
1653 # svn status won't work if the directory doesn't exist. | |
1654 self.Print(('\n________ couldn\'t run \'%s\' in \'%s\':\n' | |
1655 'The directory does not exist.') % | |
1656 (' '.join(command), self.checkout_path)) | |
1657 # There's no file list to retrieve. | |
1658 else: | |
1659 self._RunAndGetFileList(command, options, file_list) | |
1660 | |
1661 def GetUsableRev(self, rev, _options): | |
1662 """Verifies the validity of the revision for this repository.""" | |
1663 if not scm.SVN.IsValidRevision(url='%s@%s' % (self.url, rev)): | |
1664 raise NoUsableRevError( | |
1665 ( '%s isn\'t a valid revision. Please check that your safesync_url is\n' | |
1666 'correct.') % rev) | |
1667 return rev | |
1668 | |
1669 def FullUrlForRelativeUrl(self, url): | |
1670 # Find the forth '/' and strip from there. A bit hackish. | |
1671 return '/'.join(self.url.split('/')[:4]) + url | |
1672 | |
1673 def _Run(self, args, options, **kwargs): | |
1674 """Runs a commands that goes to stdout.""" | |
1675 kwargs.setdefault('cwd', self.checkout_path) | |
1676 gclient_utils.CheckCallAndFilterAndHeader(['svn'] + args, | |
1677 always=options.verbose, **kwargs) | |
1678 | |
1679 def Svnversion(self): | |
1680 """Runs the lowest checked out revision in the current project.""" | |
1681 info = scm.SVN.CaptureLocalInfo([], os.path.join(self.checkout_path, '.')) | |
1682 return info['Revision'] | |
1683 | |
1684 def _RunAndGetFileList(self, args, options, file_list, cwd=None): | |
1685 """Runs a commands that goes to stdout and grabs the file listed.""" | |
1686 cwd = cwd or self.checkout_path | |
1687 scm.SVN.RunAndGetFileList( | |
1688 options.verbose, | |
1689 args + ['--ignore-externals'], | |
1690 cwd=cwd, | |
1691 file_list=file_list) | |
1692 | |
1693 @staticmethod | |
1694 def _AddAdditionalUpdateFlags(command, options, revision): | |
1695 """Add additional flags to command depending on what options are set. | |
1696 command should be a list of strings that represents an svn command. | |
1697 | |
1698 This method returns a new list to be used as a command.""" | |
1699 new_command = command[:] | |
1700 if revision: | |
1701 new_command.extend(['--revision', str(revision).strip()]) | |
1702 # We don't want interaction when jobs are used. | |
1703 if options.jobs > 1: | |
1704 new_command.append('--non-interactive') | |
1705 # --force was added to 'svn update' in svn 1.5. | |
1706 # --accept was added to 'svn update' in svn 1.6. | |
1707 if not scm.SVN.AssertVersion('1.5')[0]: | |
1708 return new_command | |
1709 | |
1710 # It's annoying to have it block in the middle of a sync, just sensible | |
1711 # defaults. | |
1712 if options.force: | |
1713 new_command.append('--force') | |
1714 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | |
1715 new_command.extend(('--accept', 'theirs-conflict')) | |
1716 elif command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | |
1717 new_command.extend(('--accept', 'postpone')) | |
1718 return new_command | |
OLD | NEW |