| Index: gclient_scm.py
|
| diff --git a/gclient_scm.py b/gclient_scm.py
|
| index 5d12f40a154c6bcc43c960f06cea1c662decffcb..5ae69eed2baf58181da124614fa008af96a15a29 100644
|
| --- a/gclient_scm.py
|
| +++ b/gclient_scm.py
|
| @@ -4,11 +4,13 @@
|
|
|
| """Gclient-specific SCM-specific operations."""
|
|
|
| +import collections
|
| import logging
|
| import os
|
| import posixpath
|
| import re
|
| import sys
|
| +import threading
|
| import time
|
|
|
| import gclient_utils
|
| @@ -152,9 +154,51 @@ class SCMWrapper(object):
|
| return getattr(self, command)(options, args, file_list)
|
|
|
|
|
| +class GitFilter(object):
|
| + """A filter_fn implementation for quieting down git output messages.
|
| +
|
| + Allows a custom function to skip certain lines (predicate), and will throttle
|
| + the output of percentage completed lines to only output every X seconds.
|
| + """
|
| + PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*')
|
| +
|
| + def __init__(self, time_throttle=0, predicate=None):
|
| + """
|
| + Args:
|
| + time_throttle (int): GitFilter will throttle 'noisy' output (such as the
|
| + XX% complete messages) to only be printed at least |time_throttle|
|
| + seconds apart.
|
| + predicate (f(line)): An optional function which is invoked for every line.
|
| + The line will be skipped if predicate(line) returns False.
|
| + """
|
| + self.last_time = 0
|
| + self.time_throttle = time_throttle
|
| + self.predicate = predicate
|
| +
|
| + def __call__(self, line):
|
| + # git uses an escape sequence to clear the line; elide it.
|
| + esc = line.find(unichr(033))
|
| + if esc > -1:
|
| + line = line[:esc]
|
| + if self.predicate and not self.predicate(line):
|
| + return
|
| + now = time.time()
|
| + match = self.PERCENT_RE.match(line)
|
| + if not match:
|
| + self.last_time = 0
|
| + if (now - self.last_time) >= self.time_throttle:
|
| + self.last_time = now
|
| + print line
|
| +
|
| +
|
| class GitWrapper(SCMWrapper):
|
| """Wrapper for Git"""
|
|
|
| + cache_dir = None
|
| + # If a given cache is used in a solution more than once, prevent multiple
|
| + # threads from updating it simultaneously.
|
| + cache_locks = collections.defaultdict(threading.Lock)
|
| +
|
| def __init__(self, url=None, root_dir=None, relpath=None):
|
| """Removes 'git+' fake prefix from git URL."""
|
| if url.startswith('git+http://') or url.startswith('git+https://'):
|
| @@ -297,6 +341,8 @@ class GitWrapper(SCMWrapper):
|
| verbose = ['--verbose']
|
| printed_path = True
|
|
|
| + url = self._CreateOrUpdateCache(url, options)
|
| +
|
| if revision.startswith('refs/'):
|
| rev_type = "branch"
|
| elif revision.startswith('origin/'):
|
| @@ -674,6 +720,55 @@ class GitWrapper(SCMWrapper):
|
| base_url = self.url
|
| return base_url[:base_url.rfind('/')] + url
|
|
|
| + @staticmethod
|
| + def _NormalizeGitURL(url):
|
| + '''Takes a git url, strips the scheme, and ensures it ends with '.git'.'''
|
| + idx = url.find('://')
|
| + if idx != -1:
|
| + url = url[idx+3:]
|
| + if not url.endswith('.git'):
|
| + url += '.git'
|
| + return url
|
| +
|
| + def _CreateOrUpdateCache(self, url, options):
|
| + """Make a new git mirror or update existing mirror for |url|, and return the
|
| + mirror URI to clone from.
|
| +
|
| + If no cache-dir is specified, just return |url| unchanged.
|
| + """
|
| + if not self.cache_dir:
|
| + return url
|
| +
|
| + # Replace - with -- to avoid ambiguity. / with - to flatten folder structure
|
| + folder = os.path.join(
|
| + self.cache_dir,
|
| + self._NormalizeGitURL(url).replace('-', '--').replace('/', '-'))
|
| +
|
| + v = ['-v'] if options.verbose else []
|
| + filter_fn = lambda l: '[up to date]' not in l
|
| + with self.cache_locks[folder]:
|
| + gclient_utils.safe_makedirs(self.cache_dir)
|
| + if not os.path.exists(os.path.join(folder, 'config')):
|
| + gclient_utils.rmtree(folder)
|
| + self._Run(['clone'] + v + ['-c', 'core.deltaBaseCacheLimit=2g',
|
| + '--progress', '--mirror', url, folder],
|
| + options, git_filter=True, filter_fn=filter_fn,
|
| + cwd=self.cache_dir)
|
| + else:
|
| + # For now, assert that host/path/to/repo.git is identical. We may want
|
| + # to relax this restriction in the future to allow for smarter cache
|
| + # repo update schemes (such as pulling the same repo, but from a
|
| + # different host).
|
| + existing_url = self._Capture(['config', 'remote.origin.url'],
|
| + cwd=folder)
|
| + assert self._NormalizeGitURL(existing_url) == self._NormalizeGitURL(url)
|
| +
|
| + # Would normally use `git remote update`, but it doesn't support
|
| + # --progress, so use fetch instead.
|
| + self._Run(['fetch'] + v + ['--multiple', '--progress', '--all'],
|
| + options, git_filter=True, filter_fn=filter_fn, cwd=folder)
|
| + return folder
|
| +
|
| def _Clone(self, revision, url, options):
|
| """Clone a git repository from the given URL.
|
|
|
| @@ -687,6 +782,8 @@ class GitWrapper(SCMWrapper):
|
| # to stdout
|
| print('')
|
| clone_cmd = ['-c', 'core.deltaBaseCacheLimit=2g', 'clone', '--progress']
|
| + if self.cache_dir:
|
| + clone_cmd.append('--shared')
|
| if revision.startswith('refs/heads/'):
|
| clone_cmd.extend(['-b', revision.replace('refs/heads/', '')])
|
| detach_head = False
|
| @@ -702,20 +799,9 @@ class GitWrapper(SCMWrapper):
|
| if not os.path.exists(parent_dir):
|
| gclient_utils.safe_makedirs(parent_dir)
|
|
|
| - percent_re = re.compile('.* ([0-9]{1,2})% .*')
|
| - def _GitFilter(line):
|
| - # git uses an escape sequence to clear the line; elide it.
|
| - esc = line.find(unichr(033))
|
| - if esc > -1:
|
| - line = line[:esc]
|
| - match = percent_re.match(line)
|
| - if not match or not int(match.group(1)) % 10:
|
| - print '%s' % line
|
| -
|
| for _ in range(3):
|
| try:
|
| - self._Run(clone_cmd, options, cwd=self._root_dir, filter_fn=_GitFilter,
|
| - print_stdout=False)
|
| + self._Run(clone_cmd, options, cwd=self._root_dir, git_filter=True)
|
| break
|
| except subprocess2.CalledProcessError, e:
|
| # Too bad we don't have access to the actual output yet.
|
| @@ -900,13 +986,13 @@ class GitWrapper(SCMWrapper):
|
| return None
|
| return branch
|
|
|
| - def _Capture(self, args):
|
| + def _Capture(self, args, cwd=None):
|
| return subprocess2.check_output(
|
| ['git'] + args,
|
| stderr=subprocess2.VOID,
|
| nag_timer=self.nag_timer,
|
| nag_max=self.nag_max,
|
| - cwd=self.checkout_path).strip()
|
| + cwd=cwd or self.checkout_path).strip()
|
|
|
| def _UpdateBranchHeads(self, options, fetch=False):
|
| """Adds, and optionally fetches, "branch-heads" refspecs if requested."""
|
| @@ -930,11 +1016,16 @@ class GitWrapper(SCMWrapper):
|
| time.sleep(backoff_time)
|
| backoff_time *= 1.3
|
|
|
| - def _Run(self, args, options, **kwargs):
|
| + def _Run(self, args, _options, git_filter=False, **kwargs):
|
| kwargs.setdefault('cwd', self.checkout_path)
|
| - kwargs.setdefault('print_stdout', True)
|
| kwargs.setdefault('nag_timer', self.nag_timer)
|
| kwargs.setdefault('nag_max', self.nag_max)
|
| + if git_filter:
|
| + kwargs['filter_fn'] = GitFilter(kwargs['nag_timer'] / 2,
|
| + kwargs.get('filter_fn'))
|
| + kwargs.setdefault('print_stdout', False)
|
| + else:
|
| + kwargs.setdefault('print_stdout', True)
|
| stdout = kwargs.get('stdout', sys.stdout)
|
| stdout.write('\n________ running \'git %s\' in \'%s\'\n' % (
|
| ' '.join(args), kwargs['cwd']))
|
|
|