Chromium Code Reviews| 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 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 116 # SCMWrapper base class | 118 # SCMWrapper base class |
| 117 | 119 |
| 118 class SCMWrapper(object): | 120 class SCMWrapper(object): |
| 119 """Add necessary glue between all the supported SCM. | 121 """Add necessary glue between all the supported SCM. |
| 120 | 122 |
| 121 This is the abstraction layer to bind to different SCM. | 123 This is the abstraction layer to bind to different SCM. |
| 122 """ | 124 """ |
| 123 nag_timer = 30 | 125 nag_timer = 30 |
| 124 nag_max = 6 | 126 nag_max = 6 |
| 125 | 127 |
| 128 cache_dir = None | |
|
Michael Moss
2013/07/03 14:34:23
Since this is git-only, any reason not to put it i
szager1
2013/07/03 16:49:52
Yeah, I agree with mmoss.
iannucci
2013/07/03 19:07:32
... What a good idea!
And THAT's why you Always g
| |
| 129 # If a given cache is used in a solution more than once, prevent multiple | |
| 130 # threads from updating it simultaneously. | |
| 131 cache_locks = collections.defaultdict(threading.Lock) | |
| 132 | |
| 126 def __init__(self, url=None, root_dir=None, relpath=None): | 133 def __init__(self, url=None, root_dir=None, relpath=None): |
| 127 self.url = url | 134 self.url = url |
| 128 self._root_dir = root_dir | 135 self._root_dir = root_dir |
| 129 if self._root_dir: | 136 if self._root_dir: |
| 130 self._root_dir = self._root_dir.replace('/', os.sep) | 137 self._root_dir = self._root_dir.replace('/', os.sep) |
| 131 self.relpath = relpath | 138 self.relpath = relpath |
| 132 if self.relpath: | 139 if self.relpath: |
| 133 self.relpath = self.relpath.replace('/', os.sep) | 140 self.relpath = self.relpath.replace('/', os.sep) |
| 134 if self.relpath and self._root_dir: | 141 if self.relpath and self._root_dir: |
| 135 self.checkout_path = os.path.join(self._root_dir, self.relpath) | 142 self.checkout_path = os.path.join(self._root_dir, self.relpath) |
| 136 | 143 |
| 137 def RunCommand(self, command, options, args, file_list=None): | 144 def RunCommand(self, command, options, args, file_list=None): |
| 138 # file_list will have all files that are modified appended to it. | 145 # file_list will have all files that are modified appended to it. |
| 139 if file_list is None: | 146 if file_list is None: |
| 140 file_list = [] | 147 file_list = [] |
| 141 | 148 |
| 142 commands = ['cleanup', 'update', 'updatesingle', 'revert', | 149 commands = ['cleanup', 'update', 'updatesingle', 'revert', |
| 143 'revinfo', 'status', 'diff', 'pack', 'runhooks'] | 150 'revinfo', 'status', 'diff', 'pack', 'runhooks'] |
| 144 | 151 |
| 145 if not command in commands: | 152 if not command in commands: |
| 146 raise gclient_utils.Error('Unknown command %s' % command) | 153 raise gclient_utils.Error('Unknown command %s' % command) |
| 147 | 154 |
| 148 if not command in dir(self): | 155 if not command in dir(self): |
| 149 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % ( | 156 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % ( |
| 150 command, self.__class__.__name__)) | 157 command, self.__class__.__name__)) |
| 151 | 158 |
| 152 return getattr(self, command)(options, args, file_list) | 159 return getattr(self, command)(options, args, file_list) |
| 153 | 160 |
| 154 | 161 |
| 162 class GitFilter(object): | |
| 163 """A filter_fn implementation for quieting down git output messages. | |
| 164 | |
| 165 Allows a custom function to skip certain lines (predicate), and will throttle | |
| 166 the output of percentage completed lines to only output every X seconds. | |
| 167 """ | |
| 168 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*') | |
| 169 | |
| 170 def __init__(self, time_throttle=0, predicate=None): | |
| 171 """ | |
| 172 Args: | |
| 173 time_throttle (int): GitFilter will throttle 'noisy' output (such as the | |
| 174 XX% complete messages) to only be printed at least |time_throttle| | |
| 175 seconds apart. | |
| 176 predicate (f(line)): An optional function which is invoked for every line. | |
| 177 The line will be skipped if predicate(line) returns False. | |
| 178 """ | |
| 179 self.last_time = 0 | |
| 180 self.time_throttle = time_throttle | |
| 181 self.predicate = predicate | |
| 182 | |
| 183 def __call__(self, line): | |
| 184 # git uses an escape sequence to clear the line; elide it. | |
| 185 esc = line.find(unichr(033)) | |
| 186 if esc > -1: | |
| 187 line = line[:esc] | |
| 188 if self.predicate and not self.predicate(line): | |
| 189 return | |
| 190 now = time.time() | |
| 191 match = self.PERCENT_RE.match(line) | |
| 192 if not match: | |
| 193 self.last_time = 0 | |
| 194 if (now - self.last_time) >= self.time_throttle: | |
| 195 self.last_time = now | |
| 196 print line | |
| 197 | |
| 198 | |
| 155 class GitWrapper(SCMWrapper): | 199 class GitWrapper(SCMWrapper): |
| 156 """Wrapper for Git""" | 200 """Wrapper for Git""" |
| 157 | 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 |
| (...skipping 125 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/heads/'): | 346 if revision.startswith('refs/heads/'): |
| 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 374 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 684 '#Initial_checkout' ) % rev) | 730 '#Initial_checkout' ) % rev) |
| 685 | 731 |
| 686 return sha1 | 732 return sha1 |
| 687 | 733 |
| 688 def FullUrlForRelativeUrl(self, url): | 734 def FullUrlForRelativeUrl(self, url): |
| 689 # Strip from last '/' | 735 # Strip from last '/' |
| 690 # Equivalent to unix basename | 736 # Equivalent to unix basename |
| 691 base_url = self.url | 737 base_url = self.url |
| 692 return base_url[:base_url.rfind('/')] + url | 738 return base_url[:base_url.rfind('/')] + url |
| 693 | 739 |
| 740 @staticmethod | |
| 741 def _NormalizeGitURL(url): | |
| 742 '''Takes a git url, strips the scheme, and ensures it ends with '.git'.''' | |
| 743 idx = url.find('://') | |
| 744 if idx != -1: | |
| 745 url = url[idx+3:] | |
| 746 if not url.endswith('.git'): | |
| 747 url += '.git' | |
| 748 return url | |
| 749 | |
| 750 def _CreateOrUpdateCache(self, url, options): | |
| 751 """Make a new git mirror or update existing mirror for |url|, and return the | |
| 752 mirror URI to clone from. | |
| 753 | |
| 754 If no cache-dir is specified, just return |url| unchanged. | |
| 755 """ | |
| 756 if not self.cache_dir: | |
| 757 return url | |
| 758 | |
| 759 # Replace - with -- to avoid ambiguity. / with - to flatten folder structure | |
| 760 folder = os.path.join( | |
| 761 self.cache_dir, | |
| 762 self._NormalizeGitURL(url).replace('-', '--').replace('/', '-')) | |
| 763 | |
| 764 v = ['-v'] if options.verbose else [] | |
| 765 filter_fn = lambda l: '[up to date]' not in l | |
| 766 with self.cache_locks[folder]: | |
| 767 gclient_utils.safe_makedirs(self.cache_dir) | |
| 768 if not os.path.exists(os.path.join(folder, 'config')): | |
| 769 gclient_utils.rmtree(folder) | |
| 770 self._Run(['clone'] + v + ['-c', 'core.deltaBaseCacheLimit=2g', | |
| 771 '--progress', '--mirror', url, folder], | |
| 772 options, git_filter=True, filter_fn=filter_fn, | |
| 773 cwd=self.cache_dir) | |
| 774 else: | |
| 775 # For now, assert that host/path/to/repo.git is identical. We may want | |
| 776 # to relax this restriction in the future to allow for smarter cache | |
| 777 # repo update schemes (such as pulling the same repo, but from a | |
| 778 # different host). | |
| 779 existing_url = self._Capture(['config', 'remote.origin.url'], | |
| 780 cwd=folder) | |
| 781 assert self._NormalizeGitURL(existing_url) == self._NormalizeGitURL(url) | |
| 782 | |
| 783 # Would normally use `git remote update`, but it doesn't support | |
| 784 # --progress, so use fetch instead. | |
| 785 self._Run(['fetch'] + v + ['--multiple', '--progress', '--all'], | |
| 786 options, git_filter=True, filter_fn=filter_fn, cwd=folder) | |
| 787 return folder | |
| 788 | |
| 694 def _Clone(self, revision, url, options): | 789 def _Clone(self, revision, url, options): |
| 695 """Clone a git repository from the given URL. | 790 """Clone a git repository from the given URL. |
| 696 | 791 |
| 697 Once we've cloned the repo, we checkout a working branch if the specified | 792 Once we've cloned the repo, we checkout a working branch if the specified |
| 698 revision is a branch head. If it is a tag or a specific commit, then we | 793 revision is a branch head. If it is a tag or a specific commit, then we |
| 699 leave HEAD detached as it makes future updates simpler -- in this case the | 794 leave HEAD detached as it makes future updates simpler -- in this case the |
| 700 user should first create a new branch or switch to an existing branch before | 795 user should first create a new branch or switch to an existing branch before |
| 701 making changes in the repo.""" | 796 making changes in the repo.""" |
| 702 if not options.verbose: | 797 if not options.verbose: |
| 703 # git clone doesn't seem to insert a newline properly before printing | 798 # git clone doesn't seem to insert a newline properly before printing |
| 704 # to stdout | 799 # to stdout |
| 705 print('') | 800 print('') |
| 706 clone_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'clone', '--progress'] | 801 clone_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'clone', '--progress'] |
| 802 if self.cache_dir: | |
| 803 clone_cmd.append('--shared') | |
| 707 if revision.startswith('refs/heads/'): | 804 if revision.startswith('refs/heads/'): |
| 708 clone_cmd.extend(['-b', revision.replace('refs/heads/', '')]) | 805 clone_cmd.extend(['-b', revision.replace('refs/heads/', '')]) |
| 709 detach_head = False | 806 detach_head = False |
| 710 else: | 807 else: |
| 711 detach_head = True | 808 detach_head = True |
| 712 if options.verbose: | 809 if options.verbose: |
| 713 clone_cmd.append('--verbose') | 810 clone_cmd.append('--verbose') |
| 714 clone_cmd.extend([url, self.checkout_path]) | 811 clone_cmd.extend([url, self.checkout_path]) |
| 715 | 812 |
| 716 # If the parent directory does not exist, Git clone on Windows will not | 813 # If the parent directory does not exist, Git clone on Windows will not |
| 717 # create it, so we need to do it manually. | 814 # create it, so we need to do it manually. |
| 718 parent_dir = os.path.dirname(self.checkout_path) | 815 parent_dir = os.path.dirname(self.checkout_path) |
| 719 if not os.path.exists(parent_dir): | 816 if not os.path.exists(parent_dir): |
| 720 gclient_utils.safe_makedirs(parent_dir) | 817 gclient_utils.safe_makedirs(parent_dir) |
| 721 | 818 |
| 722 percent_re = re.compile('.* ([0-9]{1,2})% .*') | |
| 723 def _GitFilter(line): | |
| 724 # git uses an escape sequence to clear the line; elide it. | |
| 725 esc = line.find(unichr(033)) | |
| 726 if esc > -1: | |
| 727 line = line[:esc] | |
| 728 match = percent_re.match(line) | |
| 729 if not match or not int(match.group(1)) % 10: | |
| 730 print '%s' % line | |
| 731 | |
| 732 for _ in range(3): | 819 for _ in range(3): |
| 733 try: | 820 try: |
| 734 self._Run(clone_cmd, options, cwd=self._root_dir, filter_fn=_GitFilter, | 821 self._Run(clone_cmd, options, cwd=self._root_dir, git_filter=True) |
| 735 print_stdout=False) | |
| 736 break | 822 break |
| 737 except subprocess2.CalledProcessError, e: | 823 except subprocess2.CalledProcessError, e: |
| 738 # Too bad we don't have access to the actual output yet. | 824 # Too bad we don't have access to the actual output yet. |
| 739 # We should check for "transfer closed with NNN bytes remaining to | 825 # We should check for "transfer closed with NNN bytes remaining to |
| 740 # read". In the meantime, just make sure .git exists. | 826 # read". In the meantime, just make sure .git exists. |
| 741 if (e.returncode == 128 and | 827 if (e.returncode == 128 and |
| 742 os.path.exists(os.path.join(self.checkout_path, '.git'))): | 828 os.path.exists(os.path.join(self.checkout_path, '.git'))): |
| 743 print(str(e)) | 829 print(str(e)) |
| 744 print('Retrying...') | 830 print('Retrying...') |
| 745 continue | 831 continue |
| (...skipping 164 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 910 print('\n_____ found an unreferenced commit and saved it as \'%s\'' % | 996 print('\n_____ found an unreferenced commit and saved it as \'%s\'' % |
| 911 name) | 997 name) |
| 912 | 998 |
| 913 def _GetCurrentBranch(self): | 999 def _GetCurrentBranch(self): |
| 914 # Returns name of current branch or None for detached HEAD | 1000 # Returns name of current branch or None for detached HEAD |
| 915 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD']) | 1001 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD']) |
| 916 if branch == 'HEAD': | 1002 if branch == 'HEAD': |
| 917 return None | 1003 return None |
| 918 return branch | 1004 return branch |
| 919 | 1005 |
| 920 def _Capture(self, args): | 1006 def _Capture(self, args, cwd=None): |
| 921 return subprocess2.check_output( | 1007 return subprocess2.check_output( |
| 922 ['git'] + args, | 1008 ['git'] + args, |
| 923 stderr=subprocess2.VOID, | 1009 stderr=subprocess2.VOID, |
| 924 nag_timer=self.nag_timer, | 1010 nag_timer=self.nag_timer, |
| 925 nag_max=self.nag_max, | 1011 nag_max=self.nag_max, |
| 926 cwd=self.checkout_path).strip() | 1012 cwd=cwd or self.checkout_path).strip() |
| 927 | 1013 |
| 928 def _UpdateBranchHeads(self, options, fetch=False): | 1014 def _UpdateBranchHeads(self, options, fetch=False): |
| 929 """Adds, and optionally fetches, "branch-heads" refspecs if requested.""" | 1015 """Adds, and optionally fetches, "branch-heads" refspecs if requested.""" |
| 930 if hasattr(options, 'with_branch_heads') and options.with_branch_heads: | 1016 if hasattr(options, 'with_branch_heads') and options.with_branch_heads: |
| 931 backoff_time = 5 | 1017 backoff_time = 5 |
| 932 for _ in range(3): | 1018 for _ in range(3): |
| 933 try: | 1019 try: |
| 934 config_cmd = ['config', 'remote.origin.fetch', | 1020 config_cmd = ['config', 'remote.origin.fetch', |
| 935 '+refs/branch-heads/*:refs/remotes/branch-heads/*', | 1021 '+refs/branch-heads/*:refs/remotes/branch-heads/*', |
| 936 '^\\+refs/branch-heads/\\*:.*$'] | 1022 '^\\+refs/branch-heads/\\*:.*$'] |
| 937 self._Run(config_cmd, options) | 1023 self._Run(config_cmd, options) |
| 938 if fetch: | 1024 if fetch: |
| 939 fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin'] | 1025 fetch_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'fetch', 'origin'] |
| 940 if options.verbose: | 1026 if options.verbose: |
| 941 fetch_cmd.append('--verbose') | 1027 fetch_cmd.append('--verbose') |
| 942 self._Run(fetch_cmd, options) | 1028 self._Run(fetch_cmd, options) |
| 943 break | 1029 break |
| 944 except subprocess2.CalledProcessError, e: | 1030 except subprocess2.CalledProcessError, e: |
| 945 print(str(e)) | 1031 print(str(e)) |
| 946 print('Retrying in %.1f seconds...' % backoff_time) | 1032 print('Retrying in %.1f seconds...' % backoff_time) |
| 947 time.sleep(backoff_time) | 1033 time.sleep(backoff_time) |
| 948 backoff_time *= 1.3 | 1034 backoff_time *= 1.3 |
| 949 | 1035 |
| 950 def _Run(self, args, options, **kwargs): | 1036 def _Run(self, args, _options, git_filter=False, **kwargs): |
| 951 kwargs.setdefault('cwd', self.checkout_path) | 1037 kwargs.setdefault('cwd', self.checkout_path) |
| 952 kwargs.setdefault('print_stdout', True) | |
| 953 kwargs.setdefault('nag_timer', self.nag_timer) | 1038 kwargs.setdefault('nag_timer', self.nag_timer) |
| 954 kwargs.setdefault('nag_max', self.nag_max) | 1039 kwargs.setdefault('nag_max', self.nag_max) |
| 1040 if git_filter: | |
| 1041 kwargs['filter_fn'] = GitFilter(kwargs['nag_timer'] / 2, | |
| 1042 kwargs.get('filter_fn')) | |
| 1043 kwargs.setdefault('print_stdout', False) | |
| 1044 else: | |
| 1045 kwargs.setdefault('print_stdout', True) | |
| 955 stdout = kwargs.get('stdout', sys.stdout) | 1046 stdout = kwargs.get('stdout', sys.stdout) |
| 956 stdout.write('\n________ running \'git %s\' in \'%s\'\n' % ( | 1047 stdout.write('\n________ running \'git %s\' in \'%s\'\n' % ( |
| 957 ' '.join(args), kwargs['cwd'])) | 1048 ' '.join(args), kwargs['cwd'])) |
| 958 gclient_utils.CheckCallAndFilter(['git'] + args, **kwargs) | 1049 gclient_utils.CheckCallAndFilter(['git'] + args, **kwargs) |
| 959 | 1050 |
| 960 | 1051 |
| 961 class SVNWrapper(SCMWrapper): | 1052 class SVNWrapper(SCMWrapper): |
| 962 """ Wrapper for SVN """ | 1053 """ Wrapper for SVN """ |
| 963 | 1054 |
| 964 @staticmethod | 1055 @staticmethod |
| (...skipping 375 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1340 new_command.append('--force') | 1431 new_command.append('--force') |
| 1341 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1432 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
| 1342 new_command.extend(('--accept', 'theirs-conflict')) | 1433 new_command.extend(('--accept', 'theirs-conflict')) |
| 1343 elif options.manually_grab_svn_rev: | 1434 elif options.manually_grab_svn_rev: |
| 1344 new_command.append('--force') | 1435 new_command.append('--force') |
| 1345 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1436 if command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
| 1346 new_command.extend(('--accept', 'postpone')) | 1437 new_command.extend(('--accept', 'postpone')) |
| 1347 elif command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: | 1438 elif command[0] != 'checkout' and scm.SVN.AssertVersion('1.6')[0]: |
| 1348 new_command.extend(('--accept', 'postpone')) | 1439 new_command.extend(('--accept', 'postpone')) |
| 1349 return new_command | 1440 return new_command |
| OLD | NEW |