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

Side by Side Diff: gclient_scm.py

Issue 18328003: Add a git cache for gclient sync operations. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Make cache_dir .gclient only, and move it to GitWrapper Created 7 years, 5 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 | « gclient.py ('k') | tests/gclient_scm_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 # 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
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
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
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
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
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
OLDNEW
« no previous file with comments | « gclient.py ('k') | tests/gclient_scm_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698