blob: 8f7c4be2baa44bdaf5c739c891b308ad7ad871ec [file] [log] [blame]
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00001# coding=utf8
maruel@chromium.org9799a072012-01-11 00:26:25 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Manages a project checkout.
6
7Includes support for svn, git-svn and git.
8"""
9
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000010import fnmatch
11import logging
12import os
13import re
maruel@chromium.org5e975632011-09-29 18:07:06 +000014import shutil
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000015import subprocess
16import sys
17import tempfile
18
vapier9f343712016-06-22 07:13:20 -070019# The configparser module was renamed in Python 3.
20try:
21 import configparser
22except ImportError:
23 import ConfigParser as configparser
24
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000025import patch
26import scm
27import subprocess2
28
29
csharp@chromium.org9af0a112013-03-20 20:21:35 +000030if sys.platform in ('cygwin', 'win32'):
31 # Disable timeouts on Windows since we can't have shells with timeouts.
32 GLOBAL_TIMEOUT = None
33 FETCH_TIMEOUT = None
34else:
35 # Default timeout of 15 minutes.
36 GLOBAL_TIMEOUT = 15*60
37 # Use a larger timeout for checkout since it can be a genuinely slower
38 # operation.
39 FETCH_TIMEOUT = 30*60
40
41
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000042def get_code_review_setting(path, key,
43 codereview_settings_file='codereview.settings'):
44 """Parses codereview.settings and return the value for the key if present.
45
46 Don't cache the values in case the file is changed."""
47 # TODO(maruel): Do not duplicate code.
48 settings = {}
49 try:
50 settings_file = open(os.path.join(path, codereview_settings_file), 'r')
51 try:
52 for line in settings_file.readlines():
53 if not line or line.startswith('#'):
54 continue
55 if not ':' in line:
56 # Invalid file.
57 return None
58 k, v = line.split(':', 1)
59 settings[k.strip()] = v.strip()
60 finally:
61 settings_file.close()
maruel@chromium.org004fb712011-06-21 20:02:16 +000062 except IOError:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000063 return None
64 return settings.get(key, None)
65
66
maruel@chromium.org4dd9f722012-10-01 16:23:03 +000067def align_stdout(stdout):
68 """Returns the aligned output of multiple stdouts."""
69 output = ''
70 for item in stdout:
71 item = item.strip()
72 if not item:
73 continue
74 output += ''.join(' %s\n' % line for line in item.splitlines())
75 return output
76
77
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000078class PatchApplicationFailed(Exception):
79 """Patch failed to be applied."""
maruel@chromium.org34f68552012-05-09 19:18:36 +000080 def __init__(self, p, status):
81 super(PatchApplicationFailed, self).__init__(p, status)
82 self.patch = p
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000083 self.status = status
84
maruel@chromium.org34f68552012-05-09 19:18:36 +000085 @property
86 def filename(self):
87 if self.patch:
88 return self.patch.filename
89
90 def __str__(self):
91 out = []
92 if self.filename:
93 out.append('Failed to apply patch for %s:' % self.filename)
94 if self.status:
95 out.append(self.status)
maruel@chromium.orgcb5667a2012-10-23 19:42:10 +000096 if self.patch:
97 out.append('Patch: %s' % self.patch.dump())
maruel@chromium.org34f68552012-05-09 19:18:36 +000098 return '\n'.join(out)
99
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000100
101class CheckoutBase(object):
102 # Set to None to have verbose output.
103 VOID = subprocess2.VOID
104
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000105 def __init__(self, root_dir, project_name, post_processors):
106 """
107 Args:
108 post_processor: list of lambda(checkout, patches) to call on each of the
109 modified files.
110 """
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000111 super(CheckoutBase, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000112 self.root_dir = root_dir
113 self.project_name = project_name
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000114 if self.project_name is None:
115 self.project_path = self.root_dir
116 else:
117 self.project_path = os.path.join(self.root_dir, self.project_name)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000118 # Only used for logging purposes.
119 self._last_seen_revision = None
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000120 self.post_processors = post_processors
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000121 assert self.root_dir
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000122 assert self.project_path
maruel@chromium.org0aca0f92012-10-01 16:39:45 +0000123 assert os.path.isabs(self.project_path)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000124
125 def get_settings(self, key):
126 return get_code_review_setting(self.project_path, key)
127
maruel@chromium.org51919772011-06-12 01:27:42 +0000128 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000129 """Checks out a clean copy of the tree and removes any local modification.
130
131 This function shouldn't throw unless the remote repository is inaccessible,
132 there is no free disk space or hard issues like that.
maruel@chromium.org51919772011-06-12 01:27:42 +0000133
134 Args:
135 revision: The revision it should sync to, SCM specific.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000136 """
137 raise NotImplementedError()
138
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000139 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000140 """Applies a patch and returns the list of modified files.
141
142 This function should throw patch.UnsupportedPatchFormat or
143 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000144
145 Args:
146 patches: patch.PatchSet object.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000147 """
148 raise NotImplementedError()
149
150 def commit(self, commit_message, user):
151 """Commits the patch upstream, while impersonating 'user'."""
152 raise NotImplementedError()
153
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000154 def revisions(self, rev1, rev2):
155 """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]).
156
157 If rev2 is None, it means 'HEAD'.
158
159 Returns None if there is no link between the two.
160 """
161 raise NotImplementedError()
162
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000163
164class RawCheckout(CheckoutBase):
165 """Used to apply a patch locally without any intent to commit it.
166
167 To be used by the try server.
168 """
maruel@chromium.org51919772011-06-12 01:27:42 +0000169 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000170 """Stubbed out."""
171 pass
172
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000173 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000174 """Ignores svn properties."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000175 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000176 for p in patches:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000177 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000178 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000179 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000180 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000181 os.remove(filepath)
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000182 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000183 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000184 else:
185 dirname = os.path.dirname(p.filename)
186 full_dir = os.path.join(self.project_path, dirname)
187 if dirname and not os.path.isdir(full_dir):
188 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000189 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000190
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000191 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000192 content = p.get()
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000193 with open(filepath, 'wb') as f:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000194 f.write(content)
195 stdout.append('Added binary file %d bytes.' % len(content))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000196 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000197 if p.source_filename:
198 if not p.is_new:
199 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000200 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000201 'File has a source filename specified but is not new')
202 # Copy the file first.
203 if os.path.isfile(filepath):
204 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000205 p, 'File exist but was about to be overwriten')
maruel@chromium.org5e975632011-09-29 18:07:06 +0000206 shutil.copy2(
207 os.path.join(self.project_path, p.source_filename), filepath)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000208 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000209 if p.diff_hunks:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000210 cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel]
211 if verbose:
212 cmd.append('--verbose')
groby@chromium.org23279942013-07-12 19:32:33 +0000213 env = os.environ.copy()
214 env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch')
215 try:
216 stdout.append(
217 subprocess2.check_output(
218 cmd,
219 stdin=p.get(False),
220 stderr=subprocess2.STDOUT,
221 cwd=self.project_path,
222 timeout=GLOBAL_TIMEOUT,
223 env=env))
224 finally:
225 shutil.rmtree(env['TMPDIR'])
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000226 elif p.is_new and not os.path.exists(filepath):
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000227 # There is only a header. Just create the file.
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000228 open(filepath, 'w').close()
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000229 stdout.append('Created an empty file.')
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000230 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000231 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000232 if verbose:
233 print p.filename
234 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000235 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000236 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000237 except subprocess.CalledProcessError, e:
238 raise PatchApplicationFailed(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000239 p,
240 'While running %s;\n%s%s' % (
241 ' '.join(e.cmd),
242 align_stdout(stdout),
243 align_stdout([getattr(e, 'stdout', '')])))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000244
245 def commit(self, commit_message, user):
246 """Stubbed out."""
247 raise NotImplementedError('RawCheckout can\'t commit')
248
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000249 def revisions(self, _rev1, _rev2):
250 return None
251
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000252
253class SvnConfig(object):
254 """Parses a svn configuration file."""
255 def __init__(self, svn_config_dir=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000256 super(SvnConfig, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000257 self.svn_config_dir = svn_config_dir
258 self.default = not bool(self.svn_config_dir)
259 if not self.svn_config_dir:
260 if sys.platform == 'win32':
261 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
262 else:
mmoss@chromium.orgc3499712015-11-25 01:04:01 +0000263 self.svn_config_dir = os.path.expanduser(
264 os.path.join('~', '.subversion'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000265 svn_config_file = os.path.join(self.svn_config_dir, 'config')
vapier9f343712016-06-22 07:13:20 -0700266 parser = configparser.SafeConfigParser()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000267 if os.path.isfile(svn_config_file):
268 parser.read(svn_config_file)
269 else:
270 parser.add_section('auto-props')
271 self.auto_props = dict(parser.items('auto-props'))
272
273
274class SvnMixIn(object):
275 """MixIn class to add svn commands common to both svn and git-svn clients."""
276 # These members need to be set by the subclass.
277 commit_user = None
278 commit_pwd = None
279 svn_url = None
280 project_path = None
281 # Override at class level when necessary. If used, --non-interactive is
282 # implied.
283 svn_config = SvnConfig()
284 # Set to True when non-interactivity is necessary but a custom subversion
285 # configuration directory is not necessary.
286 non_interactive = False
287
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000288 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000289 args = ['svn'] + args
290 if not self.svn_config.default:
291 args.extend(['--config-dir', self.svn_config.svn_config_dir])
292 if not self.svn_config.default or self.non_interactive or non_interactive:
293 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000294 if credentials:
295 if self.commit_user:
296 args.extend(['--username', self.commit_user])
297 if self.commit_pwd:
298 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000299 return args
300
301 def _check_call_svn(self, args, **kwargs):
302 """Runs svn and throws an exception if the command failed."""
303 kwargs.setdefault('cwd', self.project_path)
304 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000305 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000306 return subprocess2.check_call_out(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000307 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000308
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000309 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000310 """Runs svn and throws an exception if the command failed.
311
312 Returns the output.
313 """
314 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000315 return subprocess2.check_output(
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000316 self._add_svn_flags(args, True, credentials),
317 stderr=subprocess2.STDOUT,
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000318 timeout=GLOBAL_TIMEOUT,
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000319 **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000320
321 @staticmethod
322 def _parse_svn_info(output, key):
323 """Returns value for key from svn info output.
324
325 Case insensitive.
326 """
327 values = {}
328 key = key.lower()
329 for line in output.splitlines(False):
330 if not line:
331 continue
332 k, v = line.split(':', 1)
333 k = k.strip().lower()
334 v = v.strip()
335 assert not k in values
336 values[k] = v
337 return values.get(key, None)
338
339
340class SvnCheckout(CheckoutBase, SvnMixIn):
341 """Manages a subversion checkout."""
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000342 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
343 post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000344 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
345 SvnMixIn.__init__(self)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000346 self.commit_user = commit_user
347 self.commit_pwd = commit_pwd
348 self.svn_url = svn_url
349 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000350
maruel@chromium.org51919772011-06-12 01:27:42 +0000351 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000352 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000353 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000354 if not os.path.isdir(self.project_path):
355 logging.info('Checking out %s in %s' %
356 (self.project_name, self.project_path))
maruel@chromium.org51919772011-06-12 01:27:42 +0000357 return self._revert(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000358
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000359 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000360 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000361 for p in patches:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000362 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000363 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000364 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000365 # It is important to use credentials=False otherwise credentials could
366 # leak in the error message. Credentials are not necessary here for the
367 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000368 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000369 stdout.append(self._check_output_svn(
370 ['delete', p.filename, '--force'], credentials=False))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000371 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000372 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000373 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000374 # svn add while creating directories otherwise svn add on the
375 # contained files will silently fail.
376 # First, find the root directory that exists.
377 dirname = os.path.dirname(p.filename)
378 dirs_to_create = []
379 while (dirname and
380 not os.path.isdir(os.path.join(self.project_path, dirname))):
381 dirs_to_create.append(dirname)
382 dirname = os.path.dirname(dirname)
383 for dir_to_create in reversed(dirs_to_create):
384 os.mkdir(os.path.join(self.project_path, dir_to_create))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000385 stdout.append(
386 self._check_output_svn(
387 ['add', dir_to_create, '--force'], credentials=False))
388 stdout.append('Created missing directory %s.' % dir_to_create)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000389
390 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000391 content = p.get()
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000392 with open(filepath, 'wb') as f:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000393 f.write(content)
394 stdout.append('Added binary file %d bytes.' % len(content))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000395 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000396 if p.source_filename:
397 if not p.is_new:
398 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000399 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000400 'File has a source filename specified but is not new')
401 # Copy the file first.
402 if os.path.isfile(filepath):
403 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000404 p, 'File exist but was about to be overwriten')
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000405 stdout.append(
406 self._check_output_svn(
407 ['copy', p.source_filename, p.filename]))
408 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000409 if p.diff_hunks:
maruel@chromium.orgec4a9182012-09-28 20:39:45 +0000410 cmd = [
411 'patch',
412 '-p%s' % p.patchlevel,
413 '--forward',
414 '--force',
415 '--no-backup-if-mismatch',
416 ]
groby@chromium.org23279942013-07-12 19:32:33 +0000417 env = os.environ.copy()
418 env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch')
419 try:
420 stdout.append(
421 subprocess2.check_output(
422 cmd,
423 stdin=p.get(False),
424 cwd=self.project_path,
425 timeout=GLOBAL_TIMEOUT,
426 env=env))
427 finally:
428 shutil.rmtree(env['TMPDIR'])
429
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000430 elif p.is_new and not os.path.exists(filepath):
431 # There is only a header. Just create the file if it doesn't
432 # exist.
433 open(filepath, 'w').close()
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000434 stdout.append('Created an empty file.')
maruel@chromium.org3da83172012-05-07 16:17:20 +0000435 if p.is_new and not p.source_filename:
436 # Do not run it if p.source_filename is defined, since svn copy was
437 # using above.
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000438 stdout.append(
439 self._check_output_svn(
440 ['add', p.filename, '--force'], credentials=False))
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000441 for name, value in p.svn_properties:
442 if value is None:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000443 stdout.append(
444 self._check_output_svn(
445 ['propdel', '--quiet', name, p.filename],
446 credentials=False))
447 stdout.append('Property %s deleted.' % name)
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000448 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000449 stdout.append(
450 self._check_output_svn(
451 ['propset', name, value, p.filename], credentials=False))
452 stdout.append('Property %s=%s' % (name, value))
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000453 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000454 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000455 for value in values.split(';'):
456 if '=' not in value:
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000457 params = [value, '.']
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000458 else:
459 params = value.split('=', 1)
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000460 if params[1] == '*':
461 # Works around crbug.com/150960 on Windows.
462 params[1] = '.'
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000463 stdout.append(
464 self._check_output_svn(
465 ['propset'] + params + [p.filename], credentials=False))
466 stdout.append('Property (auto) %s' % '='.join(params))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000467 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000468 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000469 if verbose:
470 print p.filename
471 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000472 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000473 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000474 except subprocess.CalledProcessError, e:
475 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000476 p,
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000477 'While running %s;\n%s%s' % (
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000478 ' '.join(e.cmd),
479 align_stdout(stdout),
480 align_stdout([getattr(e, 'stdout', '')])))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000481
482 def commit(self, commit_message, user):
483 logging.info('Committing patch for %s' % user)
484 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000485 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000486 handle, commit_filename = tempfile.mkstemp(text=True)
487 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000488 # Shouldn't assume default encoding is UTF-8. But really, if you are using
489 # anything else, you are living in another world.
490 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000491 os.close(handle)
492 # When committing, svn won't update the Revision metadata of the checkout,
493 # so if svn commit returns "Committed revision 3.", svn info will still
494 # return "Revision: 2". Since running svn update right after svn commit
495 # creates a race condition with other committers, this code _must_ parse
496 # the output of svn commit and use a regexp to grab the revision number.
497 # Note that "Committed revision N." is localized but subprocess2 forces
498 # LANGUAGE=en.
499 args = ['commit', '--file', commit_filename]
500 # realauthor is parsed by a server-side hook.
501 if user and user != self.commit_user:
502 args.extend(['--with-revprop', 'realauthor=%s' % user])
503 out = self._check_output_svn(args)
504 finally:
505 os.remove(commit_filename)
506 lines = filter(None, out.splitlines())
507 match = re.match(r'^Committed revision (\d+).$', lines[-1])
508 if not match:
509 raise PatchApplicationFailed(
510 None,
511 'Couldn\'t make sense out of svn commit message:\n' + out)
512 return int(match.group(1))
513
maruel@chromium.org51919772011-06-12 01:27:42 +0000514 def _revert(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000515 """Reverts local modifications or checks out if the directory is not
516 present. Use depot_tools's functionality to do this.
517 """
518 flags = ['--ignore-externals']
maruel@chromium.org51919772011-06-12 01:27:42 +0000519 if revision:
520 flags.extend(['--revision', str(revision)])
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000521 if os.path.isdir(self.project_path):
522 # This may remove any part (or all) of the checkout.
523 scm.SVN.Revert(self.project_path, no_ignore=True)
524
525 if os.path.isdir(self.project_path):
526 # Revive files that were deleted in scm.SVN.Revert().
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000527 self._check_call_svn(['update', '--force'] + flags,
528 timeout=FETCH_TIMEOUT)
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000529 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000530 logging.info(
531 'Directory %s is not present, checking it out.' % self.project_path)
532 self._check_call_svn(
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000533 ['checkout', self.svn_url, self.project_path] + flags, cwd=None,
534 timeout=FETCH_TIMEOUT)
maruel@chromium.org51919772011-06-12 01:27:42 +0000535 return self._get_revision()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000536
maruel@chromium.org51919772011-06-12 01:27:42 +0000537 def _get_revision(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000538 out = self._check_output_svn(['info', '.'])
maruel@chromium.org51919772011-06-12 01:27:42 +0000539 revision = int(self._parse_svn_info(out, 'revision'))
540 if revision != self._last_seen_revision:
541 logging.info('Updated to revision %d' % revision)
542 self._last_seen_revision = revision
543 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000544
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000545 def revisions(self, rev1, rev2):
546 """Returns the number of actual commits, not just the difference between
547 numbers.
548 """
549 rev2 = rev2 or 'HEAD'
550 # Revision range is inclusive and ordering doesn't matter, they'll appear in
551 # the order specified.
552 try:
553 out = self._check_output_svn(
554 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
555 except subprocess.CalledProcessError:
556 return None
557 # Ignore the '----' lines.
558 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
559
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000560
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000561class GitCheckout(CheckoutBase):
562 """Manages a git checkout."""
563 def __init__(self, root_dir, project_name, remote_branch, git_url,
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000564 commit_user, post_processors=None):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000565 super(GitCheckout, self).__init__(root_dir, project_name, post_processors)
566 self.git_url = git_url
567 self.commit_user = commit_user
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000568 self.remote_branch = remote_branch
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000569 # The working branch where patches will be applied. It will track the
570 # remote branch.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000571 self.working_branch = 'working_branch'
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000572 # There is no reason to not hardcode origin.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000573 self.remote = 'origin'
574 # There is no reason to not hardcode master.
575 self.master_branch = 'master'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000576
maruel@chromium.org51919772011-06-12 01:27:42 +0000577 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000578 """Resets the git repository in a clean state.
579
580 Checks it out if not present and deletes the working branch.
581 """
agable@chromium.org7dc11442014-03-12 22:37:32 +0000582 assert self.remote_branch
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000583 assert self.git_url
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000584
585 if not os.path.isdir(self.project_path):
586 # Clone the repo if the directory is not present.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000587 logging.info(
588 'Checking out %s in %s', self.project_name, self.project_path)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000589 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000590 ['clone', self.git_url, '-b', self.remote_branch, self.project_path],
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000591 cwd=None, timeout=FETCH_TIMEOUT)
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000592 else:
593 # Throw away all uncommitted changes in the existing checkout.
594 self._check_call_git(['checkout', self.remote_branch])
595 self._check_call_git(
596 ['reset', '--hard', '--quiet',
597 '%s/%s' % (self.remote, self.remote_branch)])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000598
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000599 if revision:
600 try:
601 # Look if the commit hash already exist. If so, we can skip a
602 # 'git fetch' call.
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000603 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000604 except subprocess.CalledProcessError:
605 self._check_call_git(
606 ['fetch', self.remote, self.remote_branch, '--quiet'])
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000607 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000608 self._check_call_git(['checkout', '--force', '--quiet', revision])
609 else:
610 branches, active = self._branches()
611 if active != self.master_branch:
612 self._check_call_git(
613 ['checkout', '--force', '--quiet', self.master_branch])
614 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000615
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000616 if self.working_branch in branches:
617 self._call_git(['branch', '-D', self.working_branch])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000618 return self._get_head_commit_hash()
619
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000620 def _sync_remote_branch(self):
621 """Syncs the remote branch."""
622 # We do a 'git pull origin master:refs/remotes/origin/master' instead of
hinoka@google.comdabbea22014-04-21 23:58:11 +0000623 # 'git pull origin master' because from the manpage for git-pull:
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000624 # A parameter <ref> without a colon is equivalent to <ref>: when
625 # pulling/fetching, so it merges <ref> into the current branch without
626 # storing the remote branch anywhere locally.
627 remote_tracked_path = 'refs/remotes/%s/%s' % (
628 self.remote, self.remote_branch)
629 self._check_call_git(
630 ['pull', self.remote,
631 '%s:%s' % (self.remote_branch, remote_tracked_path),
632 '--quiet'])
633
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000634 def _get_head_commit_hash(self):
rmistry@google.com11145db2013-10-03 12:43:40 +0000635 """Gets the current revision (in unicode) from the local branch."""
636 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip())
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000637
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000638 def apply_patch(self, patches, post_processors=None, verbose=False):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000639 """Applies a patch on 'working_branch' and switches to it.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000640
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000641 The changes remain staged on the current branch.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000642
643 Ignores svn properties and raise an exception on unexpected ones.
644 """
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000645 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000646 # It this throws, the checkout is corrupted. Maybe worth deleting it and
647 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000648 if self.remote_branch:
649 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000650 ['checkout', '-b', self.working_branch, '-t', self.remote_branch,
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000651 '--quiet'])
652
maruel@chromium.org5e975632011-09-29 18:07:06 +0000653 for index, p in enumerate(patches):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000654 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000655 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000656 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000657 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000658 if (not os.path.exists(filepath) and
maruel@chromium.org5e975632011-09-29 18:07:06 +0000659 any(p1.source_filename == p.filename for p1 in patches[0:index])):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000660 # The file was already deleted if a prior patch with file rename
661 # was already processed because 'git apply' did it for us.
maruel@chromium.org5e975632011-09-29 18:07:06 +0000662 pass
663 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000664 stdout.append(self._check_output_git(['rm', p.filename]))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000665 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000666 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000667 else:
668 dirname = os.path.dirname(p.filename)
669 full_dir = os.path.join(self.project_path, dirname)
670 if dirname and not os.path.isdir(full_dir):
671 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000672 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000673 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000674 content = p.get()
675 with open(filepath, 'wb') as f:
676 f.write(content)
677 stdout.append('Added binary file %d bytes' % len(content))
678 cmd = ['add', p.filename]
679 if verbose:
680 cmd.append('--verbose')
681 stdout.append(self._check_output_git(cmd))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000682 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000683 # No need to do anything special with p.is_new or if not
684 # p.diff_hunks. git apply manages all that already.
primiano@chromium.org49dfcde2014-09-23 08:14:39 +0000685 cmd = ['apply', '--index', '-3', '-p%s' % p.patchlevel]
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000686 if verbose:
687 cmd.append('--verbose')
688 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
hinoka@google.com64d819b2014-05-06 19:59:11 +0000689 for key, value in p.svn_properties:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000690 # Ignore some known auto-props flags through .subversion/config,
691 # bails out on the other ones.
692 # TODO(maruel): Read ~/.subversion/config and detect the rules that
693 # applies here to figure out if the property will be correctly
694 # handled.
hinoka@google.com64d819b2014-05-06 19:59:11 +0000695 stdout.append('Property %s=%s' % (key, value))
696 if not key in (
maruel@chromium.org9799a072012-01-11 00:26:25 +0000697 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000698 raise patch.UnsupportedPatchFormat(
699 p.filename,
700 'Cannot apply svn property %s to file %s.' % (
hinoka@google.com64d819b2014-05-06 19:59:11 +0000701 key, p.filename))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000702 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000703 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000704 if verbose:
705 print p.filename
706 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000707 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000708 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000709 except subprocess.CalledProcessError, e:
710 raise PatchApplicationFailed(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000711 p,
712 'While running %s;\n%s%s' % (
713 ' '.join(e.cmd),
714 align_stdout(stdout),
715 align_stdout([getattr(e, 'stdout', '')])))
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000716 found_files = self._check_output_git(
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000717 ['diff', '--ignore-submodules',
718 '--name-only', '--staged']).splitlines(False)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000719 if sorted(patches.filenames) != sorted(found_files):
720 extra_files = sorted(set(found_files) - set(patches.filenames))
721 unpatched_files = sorted(set(patches.filenames) - set(found_files))
722 if extra_files:
723 print 'Found extra files: %r' % (extra_files,)
724 if unpatched_files:
725 print 'Found unpatched files: %r' % (unpatched_files,)
726
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000727
728 def commit(self, commit_message, user):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000729 """Commits, updates the commit message and pushes."""
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000730 # TODO(hinoka): CQ no longer uses this, I think its deprecated.
731 # Delete this.
rmistry@google.combb050f62013-10-03 16:53:54 +0000732 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000733 assert isinstance(commit_message, unicode)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000734 current_branch = self._check_output_git(
735 ['rev-parse', '--abbrev-ref', 'HEAD']).strip()
736 assert current_branch == self.working_branch
hinoka@google.comdabbea22014-04-21 23:58:11 +0000737
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000738 commit_cmd = ['commit', '-m', commit_message]
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000739 if user and user != self.commit_user:
740 # We do not have the first or last name of the user, grab the username
741 # from the email and call it the original author's name.
742 # TODO(rmistry): Do not need the below if user is already in
743 # "Name <email>" format.
744 name = user.split('@')[0]
745 commit_cmd.extend(['--author', '%s <%s>' % (name, user)])
746 self._check_call_git(commit_cmd)
747
748 # Push to the remote repository.
749 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000750 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch),
agable@chromium.org39262282014-03-19 21:07:38 +0000751 '--quiet'])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000752 # Get the revision after the push.
753 revision = self._get_head_commit_hash()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000754 # Switch back to the remote_branch and sync it.
755 self._check_call_git(['checkout', self.remote_branch])
756 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000757 # Delete the working branch since we are done with it.
758 self._check_call_git(['branch', '-D', self.working_branch])
759
760 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000761
762 def _check_call_git(self, args, **kwargs):
763 kwargs.setdefault('cwd', self.project_path)
764 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000765 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000766 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000767
768 def _call_git(self, args, **kwargs):
769 """Like check_call but doesn't throw on failure."""
770 kwargs.setdefault('cwd', self.project_path)
771 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000772 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000773 return subprocess2.call(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000774
775 def _check_output_git(self, args, **kwargs):
776 kwargs.setdefault('cwd', self.project_path)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000777 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000778 return subprocess2.check_output(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000779 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000780
781 def _branches(self):
782 """Returns the list of branches and the active one."""
783 out = self._check_output_git(['branch']).splitlines(False)
784 branches = [l[2:] for l in out]
785 active = None
786 for l in out:
787 if l.startswith('*'):
788 active = l[2:]
789 break
790 return branches, active
791
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000792 def revisions(self, rev1, rev2):
793 """Returns the number of actual commits between both hash."""
794 self._fetch_remote()
795
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000796 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000797 # Revision range is ]rev1, rev2] and ordering matters.
798 try:
799 out = self._check_output_git(
800 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
801 except subprocess.CalledProcessError:
802 return None
803 return len(out.splitlines())
804
805 def _fetch_remote(self):
806 """Fetches the remote without rebasing."""
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000807 # git fetch is always verbose even with -q, so redirect its output.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000808 self._check_output_git(['fetch', self.remote, self.remote_branch],
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000809 timeout=FETCH_TIMEOUT)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000810
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000811
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000812class ReadOnlyCheckout(object):
813 """Converts a checkout into a read-only one."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000814 def __init__(self, checkout, post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000815 super(ReadOnlyCheckout, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000816 self.checkout = checkout
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000817 self.post_processors = (post_processors or []) + (
818 self.checkout.post_processors or [])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000819
maruel@chromium.org51919772011-06-12 01:27:42 +0000820 def prepare(self, revision):
821 return self.checkout.prepare(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000822
823 def get_settings(self, key):
824 return self.checkout.get_settings(key)
825
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000826 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000827 return self.checkout.apply_patch(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000828 patches, post_processors or self.post_processors, verbose)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000829
830 def commit(self, message, user): # pylint: disable=R0201
831 logging.info('Would have committed for %s with message: %s' % (
832 user, message))
833 return 'FAKE'
834
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000835 def revisions(self, rev1, rev2):
836 return self.checkout.revisions(rev1, rev2)
837
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000838 @property
839 def project_name(self):
840 return self.checkout.project_name
841
842 @property
843 def project_path(self):
844 return self.checkout.project_path