| # coding=utf8 |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Manages a project checkout. |
| |
| Includes support for svn, git-svn and git. |
| """ |
| |
| import ConfigParser |
| import fnmatch |
| import logging |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| import patch |
| import scm |
| import subprocess2 |
| |
| |
| if sys.platform in ('cygwin', 'win32'): |
| # Disable timeouts on Windows since we can't have shells with timeouts. |
| GLOBAL_TIMEOUT = None |
| FETCH_TIMEOUT = None |
| else: |
| # Default timeout of 15 minutes. |
| GLOBAL_TIMEOUT = 15*60 |
| # Use a larger timeout for checkout since it can be a genuinely slower |
| # operation. |
| FETCH_TIMEOUT = 30*60 |
| |
| |
| def get_code_review_setting(path, key, |
| codereview_settings_file='codereview.settings'): |
| """Parses codereview.settings and return the value for the key if present. |
| |
| Don't cache the values in case the file is changed.""" |
| # TODO(maruel): Do not duplicate code. |
| settings = {} |
| try: |
| settings_file = open(os.path.join(path, codereview_settings_file), 'r') |
| try: |
| for line in settings_file.readlines(): |
| if not line or line.startswith('#'): |
| continue |
| if not ':' in line: |
| # Invalid file. |
| return None |
| k, v = line.split(':', 1) |
| settings[k.strip()] = v.strip() |
| finally: |
| settings_file.close() |
| except IOError: |
| return None |
| return settings.get(key, None) |
| |
| |
| def align_stdout(stdout): |
| """Returns the aligned output of multiple stdouts.""" |
| output = '' |
| for item in stdout: |
| item = item.strip() |
| if not item: |
| continue |
| output += ''.join(' %s\n' % line for line in item.splitlines()) |
| return output |
| |
| |
| class PatchApplicationFailed(Exception): |
| """Patch failed to be applied.""" |
| def __init__(self, p, status): |
| super(PatchApplicationFailed, self).__init__(p, status) |
| self.patch = p |
| self.status = status |
| |
| @property |
| def filename(self): |
| if self.patch: |
| return self.patch.filename |
| |
| def __str__(self): |
| out = [] |
| if self.filename: |
| out.append('Failed to apply patch for %s:' % self.filename) |
| if self.status: |
| out.append(self.status) |
| if self.patch: |
| out.append('Patch: %s' % self.patch.dump()) |
| return '\n'.join(out) |
| |
| |
| class CheckoutBase(object): |
| # Set to None to have verbose output. |
| VOID = subprocess2.VOID |
| |
| def __init__(self, root_dir, project_name, post_processors): |
| """ |
| Args: |
| post_processor: list of lambda(checkout, patches) to call on each of the |
| modified files. |
| """ |
| super(CheckoutBase, self).__init__() |
| self.root_dir = root_dir |
| self.project_name = project_name |
| if self.project_name is None: |
| self.project_path = self.root_dir |
| else: |
| self.project_path = os.path.join(self.root_dir, self.project_name) |
| # Only used for logging purposes. |
| self._last_seen_revision = None |
| self.post_processors = post_processors |
| assert self.root_dir |
| assert self.project_path |
| assert os.path.isabs(self.project_path) |
| |
| def get_settings(self, key): |
| return get_code_review_setting(self.project_path, key) |
| |
| def prepare(self, revision): |
| """Checks out a clean copy of the tree and removes any local modification. |
| |
| This function shouldn't throw unless the remote repository is inaccessible, |
| there is no free disk space or hard issues like that. |
| |
| Args: |
| revision: The revision it should sync to, SCM specific. |
| """ |
| raise NotImplementedError() |
| |
| def apply_patch(self, patches, post_processors=None, verbose=False): |
| """Applies a patch and returns the list of modified files. |
| |
| This function should throw patch.UnsupportedPatchFormat or |
| PatchApplicationFailed when relevant. |
| |
| Args: |
| patches: patch.PatchSet object. |
| """ |
| raise NotImplementedError() |
| |
| def commit(self, commit_message, user): |
| """Commits the patch upstream, while impersonating 'user'.""" |
| raise NotImplementedError() |
| |
| def revisions(self, rev1, rev2): |
| """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]). |
| |
| If rev2 is None, it means 'HEAD'. |
| |
| Returns None if there is no link between the two. |
| """ |
| raise NotImplementedError() |
| |
| |
| class RawCheckout(CheckoutBase): |
| """Used to apply a patch locally without any intent to commit it. |
| |
| To be used by the try server. |
| """ |
| def prepare(self, revision): |
| """Stubbed out.""" |
| pass |
| |
| def apply_patch(self, patches, post_processors=None, verbose=False): |
| """Ignores svn properties.""" |
| post_processors = post_processors or self.post_processors or [] |
| for p in patches: |
| stdout = [] |
| try: |
| filepath = os.path.join(self.project_path, p.filename) |
| if p.is_delete: |
| os.remove(filepath) |
| stdout.append('Deleted.') |
| else: |
| dirname = os.path.dirname(p.filename) |
| full_dir = os.path.join(self.project_path, dirname) |
| if dirname and not os.path.isdir(full_dir): |
| os.makedirs(full_dir) |
| stdout.append('Created missing directory %s.' % dirname) |
| |
| if p.is_binary: |
| content = p.get() |
| with open(filepath, 'wb') as f: |
| f.write(content) |
| stdout.append('Added binary file %d bytes.' % len(content)) |
| else: |
| if p.source_filename: |
| if not p.is_new: |
| raise PatchApplicationFailed( |
| p, |
| 'File has a source filename specified but is not new') |
| # Copy the file first. |
| if os.path.isfile(filepath): |
| raise PatchApplicationFailed( |
| p, 'File exist but was about to be overwriten') |
| shutil.copy2( |
| os.path.join(self.project_path, p.source_filename), filepath) |
| stdout.append('Copied %s -> %s' % (p.source_filename, p.filename)) |
| if p.diff_hunks: |
| cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel] |
| if verbose: |
| cmd.append('--verbose') |
| env = os.environ.copy() |
| env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch') |
| try: |
| stdout.append( |
| subprocess2.check_output( |
| cmd, |
| stdin=p.get(False), |
| stderr=subprocess2.STDOUT, |
| cwd=self.project_path, |
| timeout=GLOBAL_TIMEOUT, |
| env=env)) |
| finally: |
| shutil.rmtree(env['TMPDIR']) |
| elif p.is_new and not os.path.exists(filepath): |
| # There is only a header. Just create the file. |
| open(filepath, 'w').close() |
| stdout.append('Created an empty file.') |
| for post in post_processors: |
| post(self, p) |
| if verbose: |
| print p.filename |
| print align_stdout(stdout) |
| except OSError, e: |
| raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e)) |
| except subprocess.CalledProcessError, e: |
| raise PatchApplicationFailed( |
| p, |
| 'While running %s;\n%s%s' % ( |
| ' '.join(e.cmd), |
| align_stdout(stdout), |
| align_stdout([getattr(e, 'stdout', '')]))) |
| |
| def commit(self, commit_message, user): |
| """Stubbed out.""" |
| raise NotImplementedError('RawCheckout can\'t commit') |
| |
| def revisions(self, _rev1, _rev2): |
| return None |
| |
| |
| class SvnConfig(object): |
| """Parses a svn configuration file.""" |
| def __init__(self, svn_config_dir=None): |
| super(SvnConfig, self).__init__() |
| self.svn_config_dir = svn_config_dir |
| self.default = not bool(self.svn_config_dir) |
| if not self.svn_config_dir: |
| if sys.platform == 'win32': |
| self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion') |
| else: |
| self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion') |
| svn_config_file = os.path.join(self.svn_config_dir, 'config') |
| parser = ConfigParser.SafeConfigParser() |
| if os.path.isfile(svn_config_file): |
| parser.read(svn_config_file) |
| else: |
| parser.add_section('auto-props') |
| self.auto_props = dict(parser.items('auto-props')) |
| |
| |
| class SvnMixIn(object): |
| """MixIn class to add svn commands common to both svn and git-svn clients.""" |
| # These members need to be set by the subclass. |
| commit_user = None |
| commit_pwd = None |
| svn_url = None |
| project_path = None |
| # Override at class level when necessary. If used, --non-interactive is |
| # implied. |
| svn_config = SvnConfig() |
| # Set to True when non-interactivity is necessary but a custom subversion |
| # configuration directory is not necessary. |
| non_interactive = False |
| |
| def _add_svn_flags(self, args, non_interactive, credentials=True): |
| args = ['svn'] + args |
| if not self.svn_config.default: |
| args.extend(['--config-dir', self.svn_config.svn_config_dir]) |
| if not self.svn_config.default or self.non_interactive or non_interactive: |
| args.append('--non-interactive') |
| if credentials: |
| if self.commit_user: |
| args.extend(['--username', self.commit_user]) |
| if self.commit_pwd: |
| args.extend(['--password', self.commit_pwd]) |
| return args |
| |
| def _check_call_svn(self, args, **kwargs): |
| """Runs svn and throws an exception if the command failed.""" |
| kwargs.setdefault('cwd', self.project_path) |
| kwargs.setdefault('stdout', self.VOID) |
| kwargs.setdefault('timeout', GLOBAL_TIMEOUT) |
| return subprocess2.check_call_out( |
| self._add_svn_flags(args, False), **kwargs) |
| |
| def _check_output_svn(self, args, credentials=True, **kwargs): |
| """Runs svn and throws an exception if the command failed. |
| |
| Returns the output. |
| """ |
| kwargs.setdefault('cwd', self.project_path) |
| return subprocess2.check_output( |
| self._add_svn_flags(args, True, credentials), |
| stderr=subprocess2.STDOUT, |
| timeout=GLOBAL_TIMEOUT, |
| **kwargs) |
| |
| @staticmethod |
| def _parse_svn_info(output, key): |
| """Returns value for key from svn info output. |
| |
| Case insensitive. |
| """ |
| values = {} |
| key = key.lower() |
| for line in output.splitlines(False): |
| if not line: |
| continue |
| k, v = line.split(':', 1) |
| k = k.strip().lower() |
| v = v.strip() |
| assert not k in values |
| values[k] = v |
| return values.get(key, None) |
| |
| |
| class SvnCheckout(CheckoutBase, SvnMixIn): |
| """Manages a subversion checkout.""" |
| def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url, |
| post_processors=None): |
| CheckoutBase.__init__(self, root_dir, project_name, post_processors) |
| SvnMixIn.__init__(self) |
| self.commit_user = commit_user |
| self.commit_pwd = commit_pwd |
| self.svn_url = svn_url |
| assert bool(self.commit_user) >= bool(self.commit_pwd) |
| |
| def prepare(self, revision): |
| # Will checkout if the directory is not present. |
| assert self.svn_url |
| if not os.path.isdir(self.project_path): |
| logging.info('Checking out %s in %s' % |
| (self.project_name, self.project_path)) |
| return self._revert(revision) |
| |
| def apply_patch(self, patches, post_processors=None, verbose=False): |
| post_processors = post_processors or self.post_processors or [] |
| for p in patches: |
| stdout = [] |
| try: |
| filepath = os.path.join(self.project_path, p.filename) |
| # It is important to use credentials=False otherwise credentials could |
| # leak in the error message. Credentials are not necessary here for the |
| # following commands anyway. |
| if p.is_delete: |
| stdout.append(self._check_output_svn( |
| ['delete', p.filename, '--force'], credentials=False)) |
| stdout.append('Deleted.') |
| else: |
| # svn add while creating directories otherwise svn add on the |
| # contained files will silently fail. |
| # First, find the root directory that exists. |
| dirname = os.path.dirname(p.filename) |
| dirs_to_create = [] |
| while (dirname and |
| not os.path.isdir(os.path.join(self.project_path, dirname))): |
| dirs_to_create.append(dirname) |
| dirname = os.path.dirname(dirname) |
| for dir_to_create in reversed(dirs_to_create): |
| os.mkdir(os.path.join(self.project_path, dir_to_create)) |
| stdout.append( |
| self._check_output_svn( |
| ['add', dir_to_create, '--force'], credentials=False)) |
| stdout.append('Created missing directory %s.' % dir_to_create) |
| |
| if p.is_binary: |
| content = p.get() |
| with open(filepath, 'wb') as f: |
| f.write(content) |
| stdout.append('Added binary file %d bytes.' % len(content)) |
| else: |
| if p.source_filename: |
| if not p.is_new: |
| raise PatchApplicationFailed( |
| p, |
| 'File has a source filename specified but is not new') |
| # Copy the file first. |
| if os.path.isfile(filepath): |
| raise PatchApplicationFailed( |
| p, 'File exist but was about to be overwriten') |
| stdout.append( |
| self._check_output_svn( |
| ['copy', p.source_filename, p.filename])) |
| stdout.append('Copied %s -> %s' % (p.source_filename, p.filename)) |
| if p.diff_hunks: |
| cmd = [ |
| 'patch', |
| '-p%s' % p.patchlevel, |
| '--forward', |
| '--force', |
| '--no-backup-if-mismatch', |
| ] |
| env = os.environ.copy() |
| env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch') |
| try: |
| stdout.append( |
| subprocess2.check_output( |
| cmd, |
| stdin=p.get(False), |
| cwd=self.project_path, |
| timeout=GLOBAL_TIMEOUT, |
| env=env)) |
| finally: |
| shutil.rmtree(env['TMPDIR']) |
| |
| elif p.is_new and not os.path.exists(filepath): |
| # There is only a header. Just create the file if it doesn't |
| # exist. |
| open(filepath, 'w').close() |
| stdout.append('Created an empty file.') |
| if p.is_new and not p.source_filename: |
| # Do not run it if p.source_filename is defined, since svn copy was |
| # using above. |
| stdout.append( |
| self._check_output_svn( |
| ['add', p.filename, '--force'], credentials=False)) |
| for name, value in p.svn_properties: |
| if value is None: |
| stdout.append( |
| self._check_output_svn( |
| ['propdel', '--quiet', name, p.filename], |
| credentials=False)) |
| stdout.append('Property %s deleted.' % name) |
| else: |
| stdout.append( |
| self._check_output_svn( |
| ['propset', name, value, p.filename], credentials=False)) |
| stdout.append('Property %s=%s' % (name, value)) |
| for prop, values in self.svn_config.auto_props.iteritems(): |
| if fnmatch.fnmatch(p.filename, prop): |
| for value in values.split(';'): |
| if '=' not in value: |
| params = [value, '.'] |
| else: |
| params = value.split('=', 1) |
| if params[1] == '*': |
| # Works around crbug.com/150960 on Windows. |
| params[1] = '.' |
| stdout.append( |
| self._check_output_svn( |
| ['propset'] + params + [p.filename], credentials=False)) |
| stdout.append('Property (auto) %s' % '='.join(params)) |
| for post in post_processors: |
| post(self, p) |
| if verbose: |
| print p.filename |
| print align_stdout(stdout) |
| except OSError, e: |
| raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e)) |
| except subprocess.CalledProcessError, e: |
| raise PatchApplicationFailed( |
| p, |
| 'While running %s;\n%s%s' % ( |
| ' '.join(e.cmd), |
| align_stdout(stdout), |
| align_stdout([getattr(e, 'stdout', '')]))) |
| |
| def commit(self, commit_message, user): |
| logging.info('Committing patch for %s' % user) |
| assert self.commit_user |
| assert isinstance(commit_message, unicode) |
| handle, commit_filename = tempfile.mkstemp(text=True) |
| try: |
| # Shouldn't assume default encoding is UTF-8. But really, if you are using |
| # anything else, you are living in another world. |
| os.write(handle, commit_message.encode('utf-8')) |
| os.close(handle) |
| # When committing, svn won't update the Revision metadata of the checkout, |
| # so if svn commit returns "Committed revision 3.", svn info will still |
| # return "Revision: 2". Since running svn update right after svn commit |
| # creates a race condition with other committers, this code _must_ parse |
| # the output of svn commit and use a regexp to grab the revision number. |
| # Note that "Committed revision N." is localized but subprocess2 forces |
| # LANGUAGE=en. |
| args = ['commit', '--file', commit_filename] |
| # realauthor is parsed by a server-side hook. |
| if user and user != self.commit_user: |
| args.extend(['--with-revprop', 'realauthor=%s' % user]) |
| out = self._check_output_svn(args) |
| finally: |
| os.remove(commit_filename) |
| lines = filter(None, out.splitlines()) |
| match = re.match(r'^Committed revision (\d+).$', lines[-1]) |
| if not match: |
| raise PatchApplicationFailed( |
| None, |
| 'Couldn\'t make sense out of svn commit message:\n' + out) |
| return int(match.group(1)) |
| |
| def _revert(self, revision): |
| """Reverts local modifications or checks out if the directory is not |
| present. Use depot_tools's functionality to do this. |
| """ |
| flags = ['--ignore-externals'] |
| if revision: |
| flags.extend(['--revision', str(revision)]) |
| if os.path.isdir(self.project_path): |
| # This may remove any part (or all) of the checkout. |
| scm.SVN.Revert(self.project_path, no_ignore=True) |
| |
| if os.path.isdir(self.project_path): |
| # Revive files that were deleted in scm.SVN.Revert(). |
| self._check_call_svn(['update', '--force'] + flags, |
| timeout=FETCH_TIMEOUT) |
| else: |
| logging.info( |
| 'Directory %s is not present, checking it out.' % self.project_path) |
| self._check_call_svn( |
| ['checkout', self.svn_url, self.project_path] + flags, cwd=None, |
| timeout=FETCH_TIMEOUT) |
| return self._get_revision() |
| |
| def _get_revision(self): |
| out = self._check_output_svn(['info', '.']) |
| revision = int(self._parse_svn_info(out, 'revision')) |
| if revision != self._last_seen_revision: |
| logging.info('Updated to revision %d' % revision) |
| self._last_seen_revision = revision |
| return revision |
| |
| def revisions(self, rev1, rev2): |
| """Returns the number of actual commits, not just the difference between |
| numbers. |
| """ |
| rev2 = rev2 or 'HEAD' |
| # Revision range is inclusive and ordering doesn't matter, they'll appear in |
| # the order specified. |
| try: |
| out = self._check_output_svn( |
| ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)]) |
| except subprocess.CalledProcessError: |
| return None |
| # Ignore the '----' lines. |
| return len([l for l in out.splitlines() if l.startswith('r')]) - 1 |
| |
| |
| class GitCheckoutBase(CheckoutBase): |
| """Base class for git checkout. Not to be used as-is.""" |
| def __init__(self, root_dir, project_name, remote_branch, |
| post_processors=None): |
| super(GitCheckoutBase, self).__init__( |
| root_dir, project_name, post_processors) |
| # There is no reason to not hardcode it. |
| self.remote = 'origin' |
| self.remote_branch = remote_branch |
| self.working_branch = 'working_branch' |
| |
| def prepare(self, revision): |
| """Resets the git repository in a clean state. |
| |
| Checks it out if not present and deletes the working branch. |
| """ |
| assert self.remote_branch |
| assert os.path.isdir(self.project_path) |
| self._check_call_git(['reset', '--hard', '--quiet']) |
| if revision: |
| try: |
| revision = self._check_output_git(['rev-parse', revision]) |
| except subprocess.CalledProcessError: |
| self._check_call_git( |
| ['fetch', self.remote, self.remote_branch, '--quiet']) |
| revision = self._check_output_git(['rev-parse', revision]) |
| self._check_call_git(['checkout', '--force', '--quiet', revision]) |
| else: |
| branches, active = self._branches() |
| if active != 'master': |
| self._check_call_git(['checkout', '--force', '--quiet', 'master']) |
| self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) |
| if self.working_branch in branches: |
| self._call_git(['branch', '-D', self.working_branch]) |
| |
| def apply_patch(self, patches, post_processors=None, verbose=False): |
| """Applies a patch on 'working_branch' and switch to it. |
| |
| Also commits the changes on the local branch. |
| |
| Ignores svn properties and raise an exception on unexpected ones. |
| """ |
| post_processors = post_processors or self.post_processors or [] |
| # It this throws, the checkout is corrupted. Maybe worth deleting it and |
| # trying again? |
| if self.remote_branch: |
| self._check_call_git( |
| ['checkout', '-b', self.working_branch, |
| '%s/%s' % (self.remote, self.remote_branch), '--quiet']) |
| for index, p in enumerate(patches): |
| stdout = [] |
| try: |
| filepath = os.path.join(self.project_path, p.filename) |
| if p.is_delete: |
| if (not os.path.exists(filepath) and |
| any(p1.source_filename == p.filename for p1 in patches[0:index])): |
| # The file was already deleted if a prior patch with file rename |
| # was already processed because 'git apply' did it for us. |
| pass |
| else: |
| stdout.append(self._check_output_git(['rm', p.filename])) |
| stdout.append('Deleted.') |
| else: |
| dirname = os.path.dirname(p.filename) |
| full_dir = os.path.join(self.project_path, dirname) |
| if dirname and not os.path.isdir(full_dir): |
| os.makedirs(full_dir) |
| stdout.append('Created missing directory %s.' % dirname) |
| if p.is_binary: |
| content = p.get() |
| with open(filepath, 'wb') as f: |
| f.write(content) |
| stdout.append('Added binary file %d bytes' % len(content)) |
| cmd = ['add', p.filename] |
| if verbose: |
| cmd.append('--verbose') |
| stdout.append(self._check_output_git(cmd)) |
| else: |
| # No need to do anything special with p.is_new or if not |
| # p.diff_hunks. git apply manages all that already. |
| cmd = ['apply', '--index', '-p%s' % p.patchlevel] |
| if verbose: |
| cmd.append('--verbose') |
| stdout.append(self._check_output_git(cmd, stdin=p.get(True))) |
| for name, value in p.svn_properties: |
| # Ignore some known auto-props flags through .subversion/config, |
| # bails out on the other ones. |
| # TODO(maruel): Read ~/.subversion/config and detect the rules that |
| # applies here to figure out if the property will be correctly |
| # handled. |
| stdout.append('Property %s=%s' % (name, value)) |
| if not name in ( |
| 'svn:eol-style', 'svn:executable', 'svn:mime-type'): |
| raise patch.UnsupportedPatchFormat( |
| p.filename, |
| 'Cannot apply svn property %s to file %s.' % ( |
| name, p.filename)) |
| for post in post_processors: |
| post(self, p) |
| if verbose: |
| print p.filename |
| print align_stdout(stdout) |
| except OSError, e: |
| raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e)) |
| except subprocess.CalledProcessError, e: |
| raise PatchApplicationFailed( |
| p, |
| 'While running %s;\n%s%s' % ( |
| ' '.join(e.cmd), |
| align_stdout(stdout), |
| align_stdout([getattr(e, 'stdout', '')]))) |
| # Once all the patches are processed and added to the index, commit the |
| # index. |
| cmd = ['commit', '-m', 'Committed patch'] |
| if verbose: |
| cmd.append('--verbose') |
| self._check_call_git(cmd) |
| # TODO(maruel): Weirdly enough they don't match, need to investigate. |
| #found_files = self._check_output_git( |
| # ['diff', 'master', '--name-only']).splitlines(False) |
| #assert sorted(patches.filenames) == sorted(found_files), ( |
| # sorted(out), sorted(found_files)) |
| |
| def commit(self, commit_message, user): |
| """Updates the commit message. |
| |
| Subclass needs to dcommit or push. |
| """ |
| assert isinstance(commit_message, unicode) |
| self._check_call_git(['commit', '--amend', '-m', commit_message]) |
| return self._check_output_git(['rev-parse', 'HEAD']).strip() |
| |
| def _check_call_git(self, args, **kwargs): |
| kwargs.setdefault('cwd', self.project_path) |
| kwargs.setdefault('stdout', self.VOID) |
| kwargs.setdefault('timeout', GLOBAL_TIMEOUT) |
| return subprocess2.check_call_out(['git'] + args, **kwargs) |
| |
| def _call_git(self, args, **kwargs): |
| """Like check_call but doesn't throw on failure.""" |
| kwargs.setdefault('cwd', self.project_path) |
| kwargs.setdefault('stdout', self.VOID) |
| kwargs.setdefault('timeout', GLOBAL_TIMEOUT) |
| return subprocess2.call(['git'] + args, **kwargs) |
| |
| def _check_output_git(self, args, **kwargs): |
| kwargs.setdefault('cwd', self.project_path) |
| kwargs.setdefault('timeout', GLOBAL_TIMEOUT) |
| return subprocess2.check_output( |
| ['git'] + args, stderr=subprocess2.STDOUT, **kwargs) |
| |
| def _branches(self): |
| """Returns the list of branches and the active one.""" |
| out = self._check_output_git(['branch']).splitlines(False) |
| branches = [l[2:] for l in out] |
| active = None |
| for l in out: |
| if l.startswith('*'): |
| active = l[2:] |
| break |
| return branches, active |
| |
| def revisions(self, rev1, rev2): |
| """Returns the number of actual commits between both hash.""" |
| self._fetch_remote() |
| |
| rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch) |
| # Revision range is ]rev1, rev2] and ordering matters. |
| try: |
| out = self._check_output_git( |
| ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)]) |
| except subprocess.CalledProcessError: |
| return None |
| return len(out.splitlines()) |
| |
| def _fetch_remote(self): |
| """Fetches the remote without rebasing.""" |
| raise NotImplementedError() |
| |
| |
| class GitCheckout(GitCheckoutBase): |
| """Git checkout implementation.""" |
| def _fetch_remote(self): |
| # git fetch is always verbose even with -q -q so redirect its output. |
| self._check_output_git(['fetch', self.remote, self.remote_branch], |
| timeout=FETCH_TIMEOUT) |
| |
| |
| class ReadOnlyCheckout(object): |
| """Converts a checkout into a read-only one.""" |
| def __init__(self, checkout, post_processors=None): |
| super(ReadOnlyCheckout, self).__init__() |
| self.checkout = checkout |
| self.post_processors = (post_processors or []) + ( |
| self.checkout.post_processors or []) |
| |
| def prepare(self, revision): |
| return self.checkout.prepare(revision) |
| |
| def get_settings(self, key): |
| return self.checkout.get_settings(key) |
| |
| def apply_patch(self, patches, post_processors=None, verbose=False): |
| return self.checkout.apply_patch( |
| patches, post_processors or self.post_processors, verbose) |
| |
| def commit(self, message, user): # pylint: disable=R0201 |
| logging.info('Would have committed for %s with message: %s' % ( |
| user, message)) |
| return 'FAKE' |
| |
| def revisions(self, rev1, rev2): |
| return self.checkout.revisions(rev1, rev2) |
| |
| @property |
| def project_name(self): |
| return self.checkout.project_name |
| |
| @property |
| def project_path(self): |
| return self.checkout.project_path |