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 import collections |
7 import logging | 8 import logging |
8 import os | 9 import os |
9 import posixpath | 10 import posixpath |
10 import re | 11 import re |
11 import sys | 12 import sys |
| 13 import threading |
12 import time | 14 import time |
13 | 15 |
14 import gclient_utils | 16 import gclient_utils |
15 import scm | 17 import scm |
16 import subprocess2 | 18 import subprocess2 |
17 | 19 |
18 | 20 |
19 THIS_FILE_PATH = os.path.abspath(__file__) | 21 THIS_FILE_PATH = os.path.abspath(__file__) |
20 | 22 |
21 | 23 |
(...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
145 if not command in commands: | 147 if not command in commands: |
146 raise gclient_utils.Error('Unknown command %s' % command) | 148 raise gclient_utils.Error('Unknown command %s' % command) |
147 | 149 |
148 if not command in dir(self): | 150 if not command in dir(self): |
149 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % ( | 151 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % ( |
150 command, self.__class__.__name__)) | 152 command, self.__class__.__name__)) |
151 | 153 |
152 return getattr(self, command)(options, args, file_list) | 154 return getattr(self, command)(options, args, file_list) |
153 | 155 |
154 | 156 |
| 157 class GitFilter(object): |
| 158 """A filter_fn implementation for quieting down git output messages. |
| 159 |
| 160 Allows a custom function to skip certain lines (predicate), and will throttle |
| 161 the output of percentage completed lines to only output every X seconds. |
| 162 """ |
| 163 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*') |
| 164 |
| 165 def __init__(self, time_throttle=0, predicate=None): |
| 166 """ |
| 167 Args: |
| 168 time_throttle (int): GitFilter will throttle 'noisy' output (such as the |
| 169 XX% complete messages) to only be printed at least |time_throttle| |
| 170 seconds apart. |
| 171 predicate (f(line)): An optional function which is invoked for every line. |
| 172 The line will be skipped if predicate(line) returns False. |
| 173 """ |
| 174 self.last_time = 0 |
| 175 self.time_throttle = time_throttle |
| 176 self.predicate = predicate |
| 177 |
| 178 def __call__(self, line): |
| 179 # git uses an escape sequence to clear the line; elide it. |
| 180 esc = line.find(unichr(033)) |
| 181 if esc > -1: |
| 182 line = line[:esc] |
| 183 if self.predicate and not self.predicate(line): |
| 184 return |
| 185 now = time.time() |
| 186 match = self.PERCENT_RE.match(line) |
| 187 if not match: |
| 188 self.last_time = 0 |
| 189 if (now - self.last_time) >= self.time_throttle: |
| 190 self.last_time = now |
| 191 print line |
| 192 |
| 193 |
155 class GitWrapper(SCMWrapper): | 194 class GitWrapper(SCMWrapper): |
156 """Wrapper for Git""" | 195 """Wrapper for Git""" |
157 | 196 |
| 197 cache_dir = None |
| 198 # If a given cache is used in a solution more than once, prevent multiple |
| 199 # threads from updating it simultaneously. |
| 200 cache_locks = collections.defaultdict(threading.Lock) |
| 201 |
158 def __init__(self, url=None, root_dir=None, relpath=None): | 202 def __init__(self, url=None, root_dir=None, relpath=None): |
159 """Removes 'git+' fake prefix from git URL.""" | 203 """Removes 'git+' fake prefix from git URL.""" |
160 if url.startswith('git+http://') or url.startswith('git+https://'): | 204 if url.startswith('git+http://') or url.startswith('git+https://'): |
161 url = url[4:] | 205 url = url[4:] |
162 SCMWrapper.__init__(self, url, root_dir, relpath) | 206 SCMWrapper.__init__(self, url, root_dir, relpath) |
163 | 207 |
164 @staticmethod | 208 @staticmethod |
165 def BinaryExists(): | 209 def BinaryExists(): |
166 """Returns true if the command exists.""" | 210 """Returns true if the command exists.""" |
167 try: | 211 try: |
(...skipping 122 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
290 rev_str = ' at %s' % revision | 334 rev_str = ' at %s' % revision |
291 files = [] | 335 files = [] |
292 | 336 |
293 printed_path = False | 337 printed_path = False |
294 verbose = [] | 338 verbose = [] |
295 if options.verbose: | 339 if options.verbose: |
296 print('\n_____ %s%s' % (self.relpath, rev_str)) | 340 print('\n_____ %s%s' % (self.relpath, rev_str)) |
297 verbose = ['--verbose'] | 341 verbose = ['--verbose'] |
298 printed_path = True | 342 printed_path = True |
299 | 343 |
| 344 url = self._CreateOrUpdateCache(url, options) |
| 345 |
300 if revision.startswith('refs/'): | 346 if revision.startswith('refs/'): |
301 rev_type = "branch" | 347 rev_type = "branch" |
302 elif revision.startswith('origin/'): | 348 elif revision.startswith('origin/'): |
303 # For compatability with old naming, translate 'origin' to 'refs/heads' | 349 # For compatability with old naming, translate 'origin' to 'refs/heads' |
304 revision = revision.replace('origin/', 'refs/heads/') | 350 revision = revision.replace('origin/', 'refs/heads/') |
305 rev_type = "branch" | 351 rev_type = "branch" |
306 else: | 352 else: |
307 # hash is also a tag, only make a distinction at checkout | 353 # hash is also a tag, only make a distinction at checkout |
308 rev_type = "hash" | 354 rev_type = "hash" |
309 | 355 |
(...skipping 357 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
667 '#Initial_checkout' ) % rev) | 713 '#Initial_checkout' ) % rev) |
668 | 714 |
669 return sha1 | 715 return sha1 |
670 | 716 |
671 def FullUrlForRelativeUrl(self, url): | 717 def FullUrlForRelativeUrl(self, url): |
672 # Strip from last '/' | 718 # Strip from last '/' |
673 # Equivalent to unix basename | 719 # Equivalent to unix basename |
674 base_url = self.url | 720 base_url = self.url |
675 return base_url[:base_url.rfind('/')] + url | 721 return base_url[:base_url.rfind('/')] + url |
676 | 722 |
| 723 @staticmethod |
| 724 def _NormalizeGitURL(url): |
| 725 '''Takes a git url, strips the scheme, and ensures it ends with '.git'.''' |
| 726 idx = url.find('://') |
| 727 if idx != -1: |
| 728 url = url[idx+3:] |
| 729 if not url.endswith('.git'): |
| 730 url += '.git' |
| 731 return url |
| 732 |
| 733 def _CreateOrUpdateCache(self, url, options): |
| 734 """Make a new git mirror or update existing mirror for |url|, and return the |
| 735 mirror URI to clone from. |
| 736 |
| 737 If no cache-dir is specified, just return |url| unchanged. |
| 738 """ |
| 739 if not self.cache_dir: |
| 740 return url |
| 741 |
| 742 # Replace - with -- to avoid ambiguity. / with - to flatten folder structure |
| 743 folder = os.path.join( |
| 744 self.cache_dir, |
| 745 self._NormalizeGitURL(url).replace('-', '--').replace('/', '-')) |
| 746 |
| 747 v = ['-v'] if options.verbose else [] |
| 748 filter_fn = lambda l: '[up to date]' not in l |
| 749 with self.cache_locks[folder]: |
| 750 gclient_utils.safe_makedirs(self.cache_dir) |
| 751 if not os.path.exists(os.path.join(folder, 'config')): |
| 752 gclient_utils.rmtree(folder) |
| 753 self._Run(['clone'] + v + ['-c', 'core.deltaBaseCacheLimit=2g', |
| 754 '--progress', '--mirror', url, folder], |
| 755 options, git_filter=True, filter_fn=filter_fn, |
| 756 cwd=self.cache_dir) |
| 757 else: |
| 758 # For now, assert that host/path/to/repo.git is identical. We may want |
| 759 # to relax this restriction in the future to allow for smarter cache |
| 760 # repo update schemes (such as pulling the same repo, but from a |
| 761 # different host). |
| 762 existing_url = self._Capture(['config', 'remote.origin.url'], |
| 763 cwd=folder) |
| 764 assert self._NormalizeGitURL(existing_url) == self._NormalizeGitURL(url) |
| 765 |
| 766 # Would normally use `git remote update`, but it doesn't support |
| 767 # --progress, so use fetch instead. |
| 768 self._Run(['fetch'] + v + ['--multiple', '--progress', '--all'], |
| 769 options, git_filter=True, filter_fn=filter_fn, cwd=folder) |
| 770 return folder |
| 771 |
677 def _Clone(self, revision, url, options): | 772 def _Clone(self, revision, url, options): |
678 """Clone a git repository from the given URL. | 773 """Clone a git repository from the given URL. |
679 | 774 |
680 Once we've cloned the repo, we checkout a working branch if the specified | 775 Once we've cloned the repo, we checkout a working branch if the specified |
681 revision is a branch head. If it is a tag or a specific commit, then we | 776 revision is a branch head. If it is a tag or a specific commit, then we |
682 leave HEAD detached as it makes future updates simpler -- in this case the | 777 leave HEAD detached as it makes future updates simpler -- in this case the |
683 user should first create a new branch or switch to an existing branch before | 778 user should first create a new branch or switch to an existing branch before |
684 making changes in the repo.""" | 779 making changes in the repo.""" |
685 if not options.verbose: | 780 if not options.verbose: |
686 # git clone doesn't seem to insert a newline properly before printing | 781 # git clone doesn't seem to insert a newline properly before printing |
687 # to stdout | 782 # to stdout |
688 print('') | 783 print('') |
689 clone_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'clone', '--progress'] | 784 clone_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'clone', '--progress'] |
| 785 if self.cache_dir: |
| 786 clone_cmd.append('--shared') |
690 if revision.startswith('refs/heads/'): | 787 if revision.startswith('refs/heads/'): |
691 clone_cmd.extend(['-b', revision.replace('refs/heads/', '')]) | 788 clone_cmd.extend(['-b', revision.replace('refs/heads/', '')]) |
692 detach_head = False | 789 detach_head = False |
693 else: | 790 else: |
694 detach_head = True | 791 detach_head = True |
695 if options.verbose: | 792 if options.verbose: |
696 clone_cmd.append('--verbose') | 793 clone_cmd.append('--verbose') |
697 clone_cmd.extend([url, self.checkout_path]) | 794 clone_cmd.extend([url, self.checkout_path]) |
698 | 795 |
699 # If the parent directory does not exist, Git clone on Windows will not | 796 # If the parent directory does not exist, Git clone on Windows will not |
700 # create it, so we need to do it manually. | 797 # create it, so we need to do it manually. |
701 parent_dir = os.path.dirname(self.checkout_path) | 798 parent_dir = os.path.dirname(self.checkout_path) |
702 if not os.path.exists(parent_dir): | 799 if not os.path.exists(parent_dir): |
703 gclient_utils.safe_makedirs(parent_dir) | 800 gclient_utils.safe_makedirs(parent_dir) |
704 | 801 |
705 percent_re = re.compile('.* ([0-9]{1,2})% .*') | |
706 def _GitFilter(line): | |
707 # git uses an escape sequence to clear the line; elide it. | |
708 esc = line.find(unichr(033)) | |
709 if esc > -1: | |
710 line = line[:esc] | |
711 match = percent_re.match(line) | |
712 if not match or not int(match.group(1)) % 10: | |
713 print '%s' % line | |
714 | |
715 for _ in range(3): | 802 for _ in range(3): |
716 try: | 803 try: |
717 self._Run(clone_cmd, options, cwd=self._root_dir, filter_fn=_GitFilter, | 804 self._Run(clone_cmd, options, cwd=self._root_dir, git_filter=True) |
718 print_stdout=False) | |
719 break | 805 break |
720 except subprocess2.CalledProcessError, e: | 806 except subprocess2.CalledProcessError, e: |
721 # Too bad we don't have access to the actual output yet. | 807 # Too bad we don't have access to the actual output yet. |
722 # We should check for "transfer closed with NNN bytes remaining to | 808 # We should check for "transfer closed with NNN bytes remaining to |
723 # read". In the meantime, just make sure .git exists. | 809 # read". In the meantime, just make sure .git exists. |
724 if (e.returncode == 128 and | 810 if (e.returncode == 128 and |
725 os.path.exists(os.path.join(self.checkout_path, '.git'))): | 811 os.path.exists(os.path.join(self.checkout_path, '.git'))): |
726 print(str(e)) | 812 print(str(e)) |
727 print('Retrying...') | 813 print('Retrying...') |
728 continue | 814 continue |
(...skipping 164 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
893 print('\n_____ found an unreferenced commit and saved it as \'%s\'' % | 979 print('\n_____ found an unreferenced commit and saved it as \'%s\'' % |
894 name) | 980 name) |
895 | 981 |
896 def _GetCurrentBranch(self): | 982 def _GetCurrentBranch(self): |
897 # Returns name of current branch or None for detached HEAD | 983 # Returns name of current branch or None for detached HEAD |
898 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD']) | 984 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD']) |
899 if branch == 'HEAD': | 985 if branch == 'HEAD': |
900 return None | 986 return None |
901 return branch | 987 return branch |
902 | 988 |
903 def _Capture(self, args): | 989 def _Capture(self, args, cwd=None): |
904 return subprocess2.check_output( | 990 return subprocess2.check_output( |
905 ['git'] + args, | 991 ['git'] + args, |
906 stderr=subprocess2.VOID, | 992 stderr=subprocess2.VOID, |
907 nag_timer=self.nag_timer, | 993 nag_timer=self.nag_timer, |
908 nag_max=self.nag_max, | 994 nag_max=self.nag_max, |
909 cwd=self.checkout_path).strip() | 995 cwd=cwd or self.checkout_path).strip() |
910 | 996 |
911 def _UpdateBranchHeads(self, options, fetch=False): | 997 def _UpdateBranchHeads(self, options, fetch=False): |
912 """Adds, and optionally fetches, "branch-heads" refspecs if requested.""" | 998 """Adds, and optionally fetches, "branch-heads" refspecs if requested.""" |
913 if hasattr(options, 'with_branch_heads') and options.with_branch_heads: | 999 if hasattr(options, 'with_branch_heads') and options.with_branch_heads: |
914 backoff_time = 5 | 1000 backoff_time = 5 |
915 for _ in range(3): | 1001 for _ in range(3): |
916 try: | 1002 try: |
917 config_cmd = ['config', 'remote.origin.fetch', | 1003 config_cmd = ['config', 'remote.origin.fetch', |
918 '+refs/branch-heads/*:refs/remotes/branch-heads/*', | 1004 '+refs/branch-heads/*:refs/remotes/branch-heads/*', |
919 '^\\+refs/branch-heads/\\*:.*$'] | 1005 '^\\+refs/branch-heads/\\*:.*$'] |
920 self._Run(config_cmd, options) | 1006 self._Run(config_cmd, options) |
921 if fetch: | 1007 if fetch: |
922 fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin'] | 1008 fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin'] |
923 if options.verbose: | 1009 if options.verbose: |
924 fetch_cmd.append('--verbose') | 1010 fetch_cmd.append('--verbose') |
925 self._Run(fetch_cmd, options) | 1011 self._Run(fetch_cmd, options) |
926 break | 1012 break |
927 except subprocess2.CalledProcessError, e: | 1013 except subprocess2.CalledProcessError, e: |
928 print(str(e)) | 1014 print(str(e)) |
929 print('Retrying in %.1f seconds...' % backoff_time) | 1015 print('Retrying in %.1f seconds...' % backoff_time) |
930 time.sleep(backoff_time) | 1016 time.sleep(backoff_time) |
931 backoff_time *= 1.3 | 1017 backoff_time *= 1.3 |
932 | 1018 |
933 def _Run(self, args, options, **kwargs): | 1019 def _Run(self, args, _options, git_filter=False, **kwargs): |
934 kwargs.setdefault('cwd', self.checkout_path) | 1020 kwargs.setdefault('cwd', self.checkout_path) |
935 kwargs.setdefault('print_stdout', True) | |
936 kwargs.setdefault('nag_timer', self.nag_timer) | 1021 kwargs.setdefault('nag_timer', self.nag_timer) |
937 kwargs.setdefault('nag_max', self.nag_max) | 1022 kwargs.setdefault('nag_max', self.nag_max) |
| 1023 if git_filter: |
| 1024 kwargs['filter_fn'] = GitFilter(kwargs['nag_timer'] / 2, |
| 1025 kwargs.get('filter_fn')) |
| 1026 kwargs.setdefault('print_stdout', False) |
| 1027 else: |
| 1028 kwargs.setdefault('print_stdout', True) |
938 stdout = kwargs.get('stdout', sys.stdout) | 1029 stdout = kwargs.get('stdout', sys.stdout) |
939 stdout.write('\n________ running \'git %s\' in \'%s\'\n' % ( | 1030 stdout.write('\n________ running \'git %s\' in \'%s\'\n' % ( |
940 ' '.join(args), kwargs['cwd'])) | 1031 ' '.join(args), kwargs['cwd'])) |
941 gclient_utils.CheckCallAndFilter(['git'] + args, **kwargs) | 1032 gclient_utils.CheckCallAndFilter(['git'] + args, **kwargs) |
942 | 1033 |
943 | 1034 |
944 class SVNWrapper(SCMWrapper): | 1035 class SVNWrapper(SCMWrapper): |
945 """ Wrapper for SVN """ | 1036 """ Wrapper for SVN """ |
946 | 1037 |
947 @staticmethod | 1038 @staticmethod |
(...skipping 375 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1323 new_command.append('--force') | 1414 new_command.append('--force') |
1324 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1415 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
1325 new_command.extend(('--accept', 'theirs-conflict')) | 1416 new_command.extend(('--accept', 'theirs-conflict')) |
1326 elif options.manually_grab_svn_rev: | 1417 elif options.manually_grab_svn_rev: |
1327 new_command.append('--force') | 1418 new_command.append('--force') |
1328 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1419 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
1329 new_command.extend(('--accept', 'postpone')) | 1420 new_command.extend(('--accept', 'postpone')) |
1330 elif command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1421 elif command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
1331 new_command.extend(('--accept', 'postpone')) | 1422 new_command.extend(('--accept', 'postpone')) |
1332 return new_command | 1423 return new_command |
OLD | NEW |