blob: bed4737507250693f074cda16583649574eb5119 [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 ConfigParser
11import fnmatch
12import logging
13import os
14import re
maruel@chromium.org5e975632011-09-29 18:07:06 +000015import shutil
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000016import subprocess
17import sys
18import tempfile
19
20import patch
21import scm
22import subprocess2
23
24
csharp@chromium.org9af0a112013-03-20 20:21:35 +000025if sys.platform in ('cygwin', 'win32'):
26 # Disable timeouts on Windows since we can't have shells with timeouts.
27 GLOBAL_TIMEOUT = None
28 FETCH_TIMEOUT = None
29else:
30 # Default timeout of 15 minutes.
31 GLOBAL_TIMEOUT = 15*60
32 # Use a larger timeout for checkout since it can be a genuinely slower
33 # operation.
34 FETCH_TIMEOUT = 30*60
35
36
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000037def get_code_review_setting(path, key,
38 codereview_settings_file='codereview.settings'):
39 """Parses codereview.settings and return the value for the key if present.
40
41 Don't cache the values in case the file is changed."""
42 # TODO(maruel): Do not duplicate code.
43 settings = {}
44 try:
45 settings_file = open(os.path.join(path, codereview_settings_file), 'r')
46 try:
47 for line in settings_file.readlines():
48 if not line or line.startswith('#'):
49 continue
50 if not ':' in line:
51 # Invalid file.
52 return None
53 k, v = line.split(':', 1)
54 settings[k.strip()] = v.strip()
55 finally:
56 settings_file.close()
maruel@chromium.org004fb712011-06-21 20:02:16 +000057 except IOError:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000058 return None
59 return settings.get(key, None)
60
61
maruel@chromium.org4dd9f722012-10-01 16:23:03 +000062def align_stdout(stdout):
63 """Returns the aligned output of multiple stdouts."""
64 output = ''
65 for item in stdout:
66 item = item.strip()
67 if not item:
68 continue
69 output += ''.join(' %s\n' % line for line in item.splitlines())
70 return output
71
72
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000073class PatchApplicationFailed(Exception):
74 """Patch failed to be applied."""
maruel@chromium.org34f68552012-05-09 19:18:36 +000075 def __init__(self, p, status):
76 super(PatchApplicationFailed, self).__init__(p, status)
77 self.patch = p
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000078 self.status = status
79
maruel@chromium.org34f68552012-05-09 19:18:36 +000080 @property
81 def filename(self):
82 if self.patch:
83 return self.patch.filename
84
85 def __str__(self):
86 out = []
87 if self.filename:
88 out.append('Failed to apply patch for %s:' % self.filename)
89 if self.status:
90 out.append(self.status)
maruel@chromium.orgcb5667a2012-10-23 19:42:10 +000091 if self.patch:
92 out.append('Patch: %s' % self.patch.dump())
maruel@chromium.org34f68552012-05-09 19:18:36 +000093 return '\n'.join(out)
94
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000095
96class CheckoutBase(object):
97 # Set to None to have verbose output.
98 VOID = subprocess2.VOID
99
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000100 def __init__(self, root_dir, project_name, post_processors):
101 """
102 Args:
103 post_processor: list of lambda(checkout, patches) to call on each of the
104 modified files.
105 """
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000106 super(CheckoutBase, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000107 self.root_dir = root_dir
108 self.project_name = project_name
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000109 if self.project_name is None:
110 self.project_path = self.root_dir
111 else:
112 self.project_path = os.path.join(self.root_dir, self.project_name)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000113 # Only used for logging purposes.
114 self._last_seen_revision = None
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000115 self.post_processors = post_processors
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000116 assert self.root_dir
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000117 assert self.project_path
maruel@chromium.org0aca0f92012-10-01 16:39:45 +0000118 assert os.path.isabs(self.project_path)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000119
120 def get_settings(self, key):
121 return get_code_review_setting(self.project_path, key)
122
maruel@chromium.org51919772011-06-12 01:27:42 +0000123 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000124 """Checks out a clean copy of the tree and removes any local modification.
125
126 This function shouldn't throw unless the remote repository is inaccessible,
127 there is no free disk space or hard issues like that.
maruel@chromium.org51919772011-06-12 01:27:42 +0000128
129 Args:
130 revision: The revision it should sync to, SCM specific.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000131 """
132 raise NotImplementedError()
133
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000134 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000135 """Applies a patch and returns the list of modified files.
136
137 This function should throw patch.UnsupportedPatchFormat or
138 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000139
140 Args:
141 patches: patch.PatchSet object.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000142 """
143 raise NotImplementedError()
144
145 def commit(self, commit_message, user):
146 """Commits the patch upstream, while impersonating 'user'."""
147 raise NotImplementedError()
148
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000149 def revisions(self, rev1, rev2):
150 """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]).
151
152 If rev2 is None, it means 'HEAD'.
153
154 Returns None if there is no link between the two.
155 """
156 raise NotImplementedError()
157
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000158
159class RawCheckout(CheckoutBase):
160 """Used to apply a patch locally without any intent to commit it.
161
162 To be used by the try server.
163 """
maruel@chromium.org51919772011-06-12 01:27:42 +0000164 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000165 """Stubbed out."""
166 pass
167
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000168 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000169 """Ignores svn properties."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000170 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000171 for p in patches:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000172 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000173 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000174 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000175 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000176 os.remove(filepath)
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000177 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000178 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000179 else:
180 dirname = os.path.dirname(p.filename)
181 full_dir = os.path.join(self.project_path, dirname)
182 if dirname and not os.path.isdir(full_dir):
183 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000184 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000185
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000186 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000187 content = p.get()
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000188 with open(filepath, 'wb') as f:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000189 f.write(content)
190 stdout.append('Added binary file %d bytes.' % len(content))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000191 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000192 if p.source_filename:
193 if not p.is_new:
194 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000195 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000196 'File has a source filename specified but is not new')
197 # Copy the file first.
198 if os.path.isfile(filepath):
199 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000200 p, 'File exist but was about to be overwriten')
maruel@chromium.org5e975632011-09-29 18:07:06 +0000201 shutil.copy2(
202 os.path.join(self.project_path, p.source_filename), filepath)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000203 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000204 if p.diff_hunks:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000205 cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel]
206 if verbose:
207 cmd.append('--verbose')
groby@chromium.org23279942013-07-12 19:32:33 +0000208 env = os.environ.copy()
209 env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch')
210 try:
211 stdout.append(
212 subprocess2.check_output(
213 cmd,
214 stdin=p.get(False),
215 stderr=subprocess2.STDOUT,
216 cwd=self.project_path,
217 timeout=GLOBAL_TIMEOUT,
218 env=env))
219 finally:
220 shutil.rmtree(env['TMPDIR'])
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000221 elif p.is_new and not os.path.exists(filepath):
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000222 # There is only a header. Just create the file.
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000223 open(filepath, 'w').close()
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000224 stdout.append('Created an empty file.')
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000225 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000226 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000227 if verbose:
228 print p.filename
229 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000230 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000231 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000232 except subprocess.CalledProcessError, e:
233 raise PatchApplicationFailed(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000234 p,
235 'While running %s;\n%s%s' % (
236 ' '.join(e.cmd),
237 align_stdout(stdout),
238 align_stdout([getattr(e, 'stdout', '')])))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000239
240 def commit(self, commit_message, user):
241 """Stubbed out."""
242 raise NotImplementedError('RawCheckout can\'t commit')
243
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000244 def revisions(self, _rev1, _rev2):
245 return None
246
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000247
248class SvnConfig(object):
249 """Parses a svn configuration file."""
250 def __init__(self, svn_config_dir=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000251 super(SvnConfig, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000252 self.svn_config_dir = svn_config_dir
253 self.default = not bool(self.svn_config_dir)
254 if not self.svn_config_dir:
255 if sys.platform == 'win32':
256 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
257 else:
mmoss@chromium.orgc3499712015-11-25 01:04:01 +0000258 self.svn_config_dir = os.path.expanduser(
259 os.path.join('~', '.subversion'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000260 svn_config_file = os.path.join(self.svn_config_dir, 'config')
261 parser = ConfigParser.SafeConfigParser()
262 if os.path.isfile(svn_config_file):
263 parser.read(svn_config_file)
264 else:
265 parser.add_section('auto-props')
266 self.auto_props = dict(parser.items('auto-props'))
267
268
269class SvnMixIn(object):
270 """MixIn class to add svn commands common to both svn and git-svn clients."""
271 # These members need to be set by the subclass.
272 commit_user = None
273 commit_pwd = None
274 svn_url = None
275 project_path = None
276 # Override at class level when necessary. If used, --non-interactive is
277 # implied.
278 svn_config = SvnConfig()
279 # Set to True when non-interactivity is necessary but a custom subversion
280 # configuration directory is not necessary.
281 non_interactive = False
282
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000283 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000284 args = ['svn'] + args
285 if not self.svn_config.default:
286 args.extend(['--config-dir', self.svn_config.svn_config_dir])
287 if not self.svn_config.default or self.non_interactive or non_interactive:
288 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000289 if credentials:
290 if self.commit_user:
291 args.extend(['--username', self.commit_user])
292 if self.commit_pwd:
293 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000294 return args
295
296 def _check_call_svn(self, args, **kwargs):
297 """Runs svn and throws an exception if the command failed."""
298 kwargs.setdefault('cwd', self.project_path)
299 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000300 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000301 return subprocess2.check_call_out(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000302 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000303
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000304 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000305 """Runs svn and throws an exception if the command failed.
306
307 Returns the output.
308 """
309 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000310 return subprocess2.check_output(
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000311 self._add_svn_flags(args, True, credentials),
312 stderr=subprocess2.STDOUT,
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000313 timeout=GLOBAL_TIMEOUT,
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000314 **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000315
316 @staticmethod
317 def _parse_svn_info(output, key):
318 """Returns value for key from svn info output.
319
320 Case insensitive.
321 """
322 values = {}
323 key = key.lower()
324 for line in output.splitlines(False):
325 if not line:
326 continue
327 k, v = line.split(':', 1)
328 k = k.strip().lower()
329 v = v.strip()
330 assert not k in values
331 values[k] = v
332 return values.get(key, None)
333
334
335class SvnCheckout(CheckoutBase, SvnMixIn):
336 """Manages a subversion checkout."""
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000337 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
338 post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000339 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
340 SvnMixIn.__init__(self)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000341 self.commit_user = commit_user
342 self.commit_pwd = commit_pwd
343 self.svn_url = svn_url
344 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000345
maruel@chromium.org51919772011-06-12 01:27:42 +0000346 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000347 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000348 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000349 if not os.path.isdir(self.project_path):
350 logging.info('Checking out %s in %s' %
351 (self.project_name, self.project_path))
maruel@chromium.org51919772011-06-12 01:27:42 +0000352 return self._revert(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000353
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000354 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000355 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000356 for p in patches:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000357 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000358 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000359 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000360 # It is important to use credentials=False otherwise credentials could
361 # leak in the error message. Credentials are not necessary here for the
362 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000363 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000364 stdout.append(self._check_output_svn(
365 ['delete', p.filename, '--force'], credentials=False))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000366 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000367 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000368 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000369 # svn add while creating directories otherwise svn add on the
370 # contained files will silently fail.
371 # First, find the root directory that exists.
372 dirname = os.path.dirname(p.filename)
373 dirs_to_create = []
374 while (dirname and
375 not os.path.isdir(os.path.join(self.project_path, dirname))):
376 dirs_to_create.append(dirname)
377 dirname = os.path.dirname(dirname)
378 for dir_to_create in reversed(dirs_to_create):
379 os.mkdir(os.path.join(self.project_path, dir_to_create))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000380 stdout.append(
381 self._check_output_svn(
382 ['add', dir_to_create, '--force'], credentials=False))
383 stdout.append('Created missing directory %s.' % dir_to_create)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000384
385 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000386 content = p.get()
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000387 with open(filepath, 'wb') as f:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000388 f.write(content)
389 stdout.append('Added binary file %d bytes.' % len(content))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000390 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000391 if p.source_filename:
392 if not p.is_new:
393 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000394 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000395 'File has a source filename specified but is not new')
396 # Copy the file first.
397 if os.path.isfile(filepath):
398 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000399 p, 'File exist but was about to be overwriten')
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000400 stdout.append(
401 self._check_output_svn(
402 ['copy', p.source_filename, p.filename]))
403 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000404 if p.diff_hunks:
maruel@chromium.orgec4a9182012-09-28 20:39:45 +0000405 cmd = [
406 'patch',
407 '-p%s' % p.patchlevel,
408 '--forward',
409 '--force',
410 '--no-backup-if-mismatch',
411 ]
groby@chromium.org23279942013-07-12 19:32:33 +0000412 env = os.environ.copy()
413 env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch')
414 try:
415 stdout.append(
416 subprocess2.check_output(
417 cmd,
418 stdin=p.get(False),
419 cwd=self.project_path,
420 timeout=GLOBAL_TIMEOUT,
421 env=env))
422 finally:
423 shutil.rmtree(env['TMPDIR'])
424
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000425 elif p.is_new and not os.path.exists(filepath):
426 # There is only a header. Just create the file if it doesn't
427 # exist.
428 open(filepath, 'w').close()
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000429 stdout.append('Created an empty file.')
maruel@chromium.org3da83172012-05-07 16:17:20 +0000430 if p.is_new and not p.source_filename:
431 # Do not run it if p.source_filename is defined, since svn copy was
432 # using above.
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000433 stdout.append(
434 self._check_output_svn(
435 ['add', p.filename, '--force'], credentials=False))
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000436 for name, value in p.svn_properties:
437 if value is None:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000438 stdout.append(
439 self._check_output_svn(
440 ['propdel', '--quiet', name, p.filename],
441 credentials=False))
442 stdout.append('Property %s deleted.' % name)
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000443 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000444 stdout.append(
445 self._check_output_svn(
446 ['propset', name, value, p.filename], credentials=False))
447 stdout.append('Property %s=%s' % (name, value))
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000448 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000449 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000450 for value in values.split(';'):
451 if '=' not in value:
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000452 params = [value, '.']
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000453 else:
454 params = value.split('=', 1)
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000455 if params[1] == '*':
456 # Works around crbug.com/150960 on Windows.
457 params[1] = '.'
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000458 stdout.append(
459 self._check_output_svn(
460 ['propset'] + params + [p.filename], credentials=False))
461 stdout.append('Property (auto) %s' % '='.join(params))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000462 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000463 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000464 if verbose:
465 print p.filename
466 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000467 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000468 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000469 except subprocess.CalledProcessError, e:
470 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000471 p,
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000472 'While running %s;\n%s%s' % (
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000473 ' '.join(e.cmd),
474 align_stdout(stdout),
475 align_stdout([getattr(e, 'stdout', '')])))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000476
477 def commit(self, commit_message, user):
478 logging.info('Committing patch for %s' % user)
479 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000480 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000481 handle, commit_filename = tempfile.mkstemp(text=True)
482 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000483 # Shouldn't assume default encoding is UTF-8. But really, if you are using
484 # anything else, you are living in another world.
485 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000486 os.close(handle)
487 # When committing, svn won't update the Revision metadata of the checkout,
488 # so if svn commit returns "Committed revision 3.", svn info will still
489 # return "Revision: 2". Since running svn update right after svn commit
490 # creates a race condition with other committers, this code _must_ parse
491 # the output of svn commit and use a regexp to grab the revision number.
492 # Note that "Committed revision N." is localized but subprocess2 forces
493 # LANGUAGE=en.
494 args = ['commit', '--file', commit_filename]
495 # realauthor is parsed by a server-side hook.
496 if user and user != self.commit_user:
497 args.extend(['--with-revprop', 'realauthor=%s' % user])
498 out = self._check_output_svn(args)
499 finally:
500 os.remove(commit_filename)
501 lines = filter(None, out.splitlines())
502 match = re.match(r'^Committed revision (\d+).$', lines[-1])
503 if not match:
504 raise PatchApplicationFailed(
505 None,
506 'Couldn\'t make sense out of svn commit message:\n' + out)
507 return int(match.group(1))
508
maruel@chromium.org51919772011-06-12 01:27:42 +0000509 def _revert(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000510 """Reverts local modifications or checks out if the directory is not
511 present. Use depot_tools's functionality to do this.
512 """
513 flags = ['--ignore-externals']
maruel@chromium.org51919772011-06-12 01:27:42 +0000514 if revision:
515 flags.extend(['--revision', str(revision)])
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000516 if os.path.isdir(self.project_path):
517 # This may remove any part (or all) of the checkout.
518 scm.SVN.Revert(self.project_path, no_ignore=True)
519
520 if os.path.isdir(self.project_path):
521 # Revive files that were deleted in scm.SVN.Revert().
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000522 self._check_call_svn(['update', '--force'] + flags,
523 timeout=FETCH_TIMEOUT)
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000524 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000525 logging.info(
526 'Directory %s is not present, checking it out.' % self.project_path)
527 self._check_call_svn(
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000528 ['checkout', self.svn_url, self.project_path] + flags, cwd=None,
529 timeout=FETCH_TIMEOUT)
maruel@chromium.org51919772011-06-12 01:27:42 +0000530 return self._get_revision()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000531
maruel@chromium.org51919772011-06-12 01:27:42 +0000532 def _get_revision(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000533 out = self._check_output_svn(['info', '.'])
maruel@chromium.org51919772011-06-12 01:27:42 +0000534 revision = int(self._parse_svn_info(out, 'revision'))
535 if revision != self._last_seen_revision:
536 logging.info('Updated to revision %d' % revision)
537 self._last_seen_revision = revision
538 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000539
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000540 def revisions(self, rev1, rev2):
541 """Returns the number of actual commits, not just the difference between
542 numbers.
543 """
544 rev2 = rev2 or 'HEAD'
545 # Revision range is inclusive and ordering doesn't matter, they'll appear in
546 # the order specified.
547 try:
548 out = self._check_output_svn(
549 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
550 except subprocess.CalledProcessError:
551 return None
552 # Ignore the '----' lines.
553 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
554
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000555
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000556class GitCheckout(CheckoutBase):
557 """Manages a git checkout."""
558 def __init__(self, root_dir, project_name, remote_branch, git_url,
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000559 commit_user, post_processors=None):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000560 super(GitCheckout, self).__init__(root_dir, project_name, post_processors)
561 self.git_url = git_url
562 self.commit_user = commit_user
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000563 self.remote_branch = remote_branch
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000564 # The working branch where patches will be applied. It will track the
565 # remote branch.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000566 self.working_branch = 'working_branch'
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000567 # There is no reason to not hardcode origin.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000568 self.remote = 'origin'
569 # There is no reason to not hardcode master.
570 self.master_branch = 'master'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000571
maruel@chromium.org51919772011-06-12 01:27:42 +0000572 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000573 """Resets the git repository in a clean state.
574
575 Checks it out if not present and deletes the working branch.
576 """
agable@chromium.org7dc11442014-03-12 22:37:32 +0000577 assert self.remote_branch
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000578 assert self.git_url
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000579
580 if not os.path.isdir(self.project_path):
581 # Clone the repo if the directory is not present.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000582 logging.info(
583 'Checking out %s in %s', self.project_name, self.project_path)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000584 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000585 ['clone', self.git_url, '-b', self.remote_branch, self.project_path],
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000586 cwd=None, timeout=FETCH_TIMEOUT)
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000587 else:
588 # Throw away all uncommitted changes in the existing checkout.
589 self._check_call_git(['checkout', self.remote_branch])
590 self._check_call_git(
591 ['reset', '--hard', '--quiet',
592 '%s/%s' % (self.remote, self.remote_branch)])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000593
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000594 if revision:
595 try:
596 # Look if the commit hash already exist. If so, we can skip a
597 # 'git fetch' call.
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000598 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000599 except subprocess.CalledProcessError:
600 self._check_call_git(
601 ['fetch', self.remote, self.remote_branch, '--quiet'])
halton.huo@intel.com323ec372014-06-17 01:50:37 +0000602 revision = self._check_output_git(['rev-parse', revision]).rstrip()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000603 self._check_call_git(['checkout', '--force', '--quiet', revision])
604 else:
605 branches, active = self._branches()
606 if active != self.master_branch:
607 self._check_call_git(
608 ['checkout', '--force', '--quiet', self.master_branch])
609 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000610
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000611 if self.working_branch in branches:
612 self._call_git(['branch', '-D', self.working_branch])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000613 return self._get_head_commit_hash()
614
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000615 def _sync_remote_branch(self):
616 """Syncs the remote branch."""
617 # We do a 'git pull origin master:refs/remotes/origin/master' instead of
hinoka@google.comdabbea22014-04-21 23:58:11 +0000618 # 'git pull origin master' because from the manpage for git-pull:
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000619 # A parameter <ref> without a colon is equivalent to <ref>: when
620 # pulling/fetching, so it merges <ref> into the current branch without
621 # storing the remote branch anywhere locally.
622 remote_tracked_path = 'refs/remotes/%s/%s' % (
623 self.remote, self.remote_branch)
624 self._check_call_git(
625 ['pull', self.remote,
626 '%s:%s' % (self.remote_branch, remote_tracked_path),
627 '--quiet'])
628
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000629 def _get_head_commit_hash(self):
rmistry@google.com11145db2013-10-03 12:43:40 +0000630 """Gets the current revision (in unicode) from the local branch."""
631 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip())
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000632
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000633 def apply_patch(self, patches, post_processors=None, verbose=False):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000634 """Applies a patch on 'working_branch' and switches to it.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000635
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000636 The changes remain staged on the current branch.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000637
638 Ignores svn properties and raise an exception on unexpected ones.
639 """
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000640 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000641 # It this throws, the checkout is corrupted. Maybe worth deleting it and
642 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000643 if self.remote_branch:
644 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000645 ['checkout', '-b', self.working_branch, '-t', self.remote_branch,
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000646 '--quiet'])
647
maruel@chromium.org5e975632011-09-29 18:07:06 +0000648 for index, p in enumerate(patches):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000649 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000650 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000651 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000652 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000653 if (not os.path.exists(filepath) and
maruel@chromium.org5e975632011-09-29 18:07:06 +0000654 any(p1.source_filename == p.filename for p1 in patches[0:index])):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000655 # The file was already deleted if a prior patch with file rename
656 # was already processed because 'git apply' did it for us.
maruel@chromium.org5e975632011-09-29 18:07:06 +0000657 pass
658 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000659 stdout.append(self._check_output_git(['rm', p.filename]))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000660 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000661 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000662 else:
663 dirname = os.path.dirname(p.filename)
664 full_dir = os.path.join(self.project_path, dirname)
665 if dirname and not os.path.isdir(full_dir):
666 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000667 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000668 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000669 content = p.get()
670 with open(filepath, 'wb') as f:
671 f.write(content)
672 stdout.append('Added binary file %d bytes' % len(content))
673 cmd = ['add', p.filename]
674 if verbose:
675 cmd.append('--verbose')
676 stdout.append(self._check_output_git(cmd))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000677 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000678 # No need to do anything special with p.is_new or if not
679 # p.diff_hunks. git apply manages all that already.
primiano@chromium.org49dfcde2014-09-23 08:14:39 +0000680 cmd = ['apply', '--index', '-3', '-p%s' % p.patchlevel]
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000681 if verbose:
682 cmd.append('--verbose')
683 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
hinoka@google.com64d819b2014-05-06 19:59:11 +0000684 for key, value in p.svn_properties:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000685 # Ignore some known auto-props flags through .subversion/config,
686 # bails out on the other ones.
687 # TODO(maruel): Read ~/.subversion/config and detect the rules that
688 # applies here to figure out if the property will be correctly
689 # handled.
hinoka@google.com64d819b2014-05-06 19:59:11 +0000690 stdout.append('Property %s=%s' % (key, value))
691 if not key in (
maruel@chromium.org9799a072012-01-11 00:26:25 +0000692 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000693 raise patch.UnsupportedPatchFormat(
694 p.filename,
695 'Cannot apply svn property %s to file %s.' % (
hinoka@google.com64d819b2014-05-06 19:59:11 +0000696 key, p.filename))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000697 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000698 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000699 if verbose:
700 print p.filename
701 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000702 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000703 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000704 except subprocess.CalledProcessError, e:
705 raise PatchApplicationFailed(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000706 p,
707 'While running %s;\n%s%s' % (
708 ' '.join(e.cmd),
709 align_stdout(stdout),
710 align_stdout([getattr(e, 'stdout', '')])))
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000711 found_files = self._check_output_git(
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000712 ['diff', '--ignore-submodules',
713 '--name-only', '--staged']).splitlines(False)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000714 if sorted(patches.filenames) != sorted(found_files):
715 extra_files = sorted(set(found_files) - set(patches.filenames))
716 unpatched_files = sorted(set(patches.filenames) - set(found_files))
717 if extra_files:
718 print 'Found extra files: %r' % (extra_files,)
719 if unpatched_files:
720 print 'Found unpatched files: %r' % (unpatched_files,)
721
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000722
723 def commit(self, commit_message, user):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000724 """Commits, updates the commit message and pushes."""
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000725 # TODO(hinoka): CQ no longer uses this, I think its deprecated.
726 # Delete this.
rmistry@google.combb050f62013-10-03 16:53:54 +0000727 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000728 assert isinstance(commit_message, unicode)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000729 current_branch = self._check_output_git(
730 ['rev-parse', '--abbrev-ref', 'HEAD']).strip()
731 assert current_branch == self.working_branch
hinoka@google.comdabbea22014-04-21 23:58:11 +0000732
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000733 commit_cmd = ['commit', '-m', commit_message]
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000734 if user and user != self.commit_user:
735 # We do not have the first or last name of the user, grab the username
736 # from the email and call it the original author's name.
737 # TODO(rmistry): Do not need the below if user is already in
738 # "Name <email>" format.
739 name = user.split('@')[0]
740 commit_cmd.extend(['--author', '%s <%s>' % (name, user)])
741 self._check_call_git(commit_cmd)
742
743 # Push to the remote repository.
744 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000745 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch),
agable@chromium.org39262282014-03-19 21:07:38 +0000746 '--quiet'])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000747 # Get the revision after the push.
748 revision = self._get_head_commit_hash()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000749 # Switch back to the remote_branch and sync it.
750 self._check_call_git(['checkout', self.remote_branch])
751 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000752 # Delete the working branch since we are done with it.
753 self._check_call_git(['branch', '-D', self.working_branch])
754
755 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000756
757 def _check_call_git(self, args, **kwargs):
758 kwargs.setdefault('cwd', self.project_path)
759 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000760 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000761 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000762
763 def _call_git(self, args, **kwargs):
764 """Like check_call but doesn't throw on failure."""
765 kwargs.setdefault('cwd', self.project_path)
766 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000767 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000768 return subprocess2.call(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000769
770 def _check_output_git(self, args, **kwargs):
771 kwargs.setdefault('cwd', self.project_path)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000772 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000773 return subprocess2.check_output(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000774 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000775
776 def _branches(self):
777 """Returns the list of branches and the active one."""
778 out = self._check_output_git(['branch']).splitlines(False)
779 branches = [l[2:] for l in out]
780 active = None
781 for l in out:
782 if l.startswith('*'):
783 active = l[2:]
784 break
785 return branches, active
786
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000787 def revisions(self, rev1, rev2):
788 """Returns the number of actual commits between both hash."""
789 self._fetch_remote()
790
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000791 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000792 # Revision range is ]rev1, rev2] and ordering matters.
793 try:
794 out = self._check_output_git(
795 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
796 except subprocess.CalledProcessError:
797 return None
798 return len(out.splitlines())
799
800 def _fetch_remote(self):
801 """Fetches the remote without rebasing."""
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000802 # git fetch is always verbose even with -q, so redirect its output.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000803 self._check_output_git(['fetch', self.remote, self.remote_branch],
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000804 timeout=FETCH_TIMEOUT)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000805
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000806
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000807class ReadOnlyCheckout(object):
808 """Converts a checkout into a read-only one."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000809 def __init__(self, checkout, post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000810 super(ReadOnlyCheckout, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000811 self.checkout = checkout
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000812 self.post_processors = (post_processors or []) + (
813 self.checkout.post_processors or [])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000814
maruel@chromium.org51919772011-06-12 01:27:42 +0000815 def prepare(self, revision):
816 return self.checkout.prepare(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000817
818 def get_settings(self, key):
819 return self.checkout.get_settings(key)
820
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000821 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000822 return self.checkout.apply_patch(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000823 patches, post_processors or self.post_processors, verbose)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000824
825 def commit(self, message, user): # pylint: disable=R0201
826 logging.info('Would have committed for %s with message: %s' % (
827 user, message))
828 return 'FAKE'
829
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000830 def revisions(self, rev1, rev2):
831 return self.checkout.revisions(rev1, rev2)
832
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000833 @property
834 def project_name(self):
835 return self.checkout.project_name
836
837 @property
838 def project_path(self):
839 return self.checkout.project_path