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

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: Capture not Run 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
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 94 matching lines...) Expand 10 before | Expand all | Expand 10 after
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
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
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
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
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
OLDNEW
« gclient.py ('K') | « gclient.py ('k') | tests/gclient_scm_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698