blob: 5ba4ba93d3fc4d1db6880523977d611c5ec45b1a [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:
258 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
259 svn_config_file = os.path.join(self.svn_config_dir, 'config')
260 parser = ConfigParser.SafeConfigParser()
261 if os.path.isfile(svn_config_file):
262 parser.read(svn_config_file)
263 else:
264 parser.add_section('auto-props')
265 self.auto_props = dict(parser.items('auto-props'))
266
267
268class SvnMixIn(object):
269 """MixIn class to add svn commands common to both svn and git-svn clients."""
270 # These members need to be set by the subclass.
271 commit_user = None
272 commit_pwd = None
273 svn_url = None
274 project_path = None
275 # Override at class level when necessary. If used, --non-interactive is
276 # implied.
277 svn_config = SvnConfig()
278 # Set to True when non-interactivity is necessary but a custom subversion
279 # configuration directory is not necessary.
280 non_interactive = False
281
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000282 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000283 args = ['svn'] + args
284 if not self.svn_config.default:
285 args.extend(['--config-dir', self.svn_config.svn_config_dir])
286 if not self.svn_config.default or self.non_interactive or non_interactive:
287 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000288 if credentials:
289 if self.commit_user:
290 args.extend(['--username', self.commit_user])
291 if self.commit_pwd:
292 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000293 return args
294
295 def _check_call_svn(self, args, **kwargs):
296 """Runs svn and throws an exception if the command failed."""
297 kwargs.setdefault('cwd', self.project_path)
298 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000299 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000300 return subprocess2.check_call_out(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000301 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000302
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000303 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000304 """Runs svn and throws an exception if the command failed.
305
306 Returns the output.
307 """
308 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000309 return subprocess2.check_output(
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000310 self._add_svn_flags(args, True, credentials),
311 stderr=subprocess2.STDOUT,
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000312 timeout=GLOBAL_TIMEOUT,
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000313 **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000314
315 @staticmethod
316 def _parse_svn_info(output, key):
317 """Returns value for key from svn info output.
318
319 Case insensitive.
320 """
321 values = {}
322 key = key.lower()
323 for line in output.splitlines(False):
324 if not line:
325 continue
326 k, v = line.split(':', 1)
327 k = k.strip().lower()
328 v = v.strip()
329 assert not k in values
330 values[k] = v
331 return values.get(key, None)
332
333
334class SvnCheckout(CheckoutBase, SvnMixIn):
335 """Manages a subversion checkout."""
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000336 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
337 post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000338 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
339 SvnMixIn.__init__(self)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000340 self.commit_user = commit_user
341 self.commit_pwd = commit_pwd
342 self.svn_url = svn_url
343 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000344
maruel@chromium.org51919772011-06-12 01:27:42 +0000345 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000346 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000347 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000348 if not os.path.isdir(self.project_path):
349 logging.info('Checking out %s in %s' %
350 (self.project_name, self.project_path))
maruel@chromium.org51919772011-06-12 01:27:42 +0000351 return self._revert(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000352
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000353 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000354 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000355 for p in patches:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000356 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000357 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000358 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000359 # It is important to use credentials=False otherwise credentials could
360 # leak in the error message. Credentials are not necessary here for the
361 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000362 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000363 stdout.append(self._check_output_svn(
364 ['delete', p.filename, '--force'], credentials=False))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000365 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000366 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000367 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000368 # svn add while creating directories otherwise svn add on the
369 # contained files will silently fail.
370 # First, find the root directory that exists.
371 dirname = os.path.dirname(p.filename)
372 dirs_to_create = []
373 while (dirname and
374 not os.path.isdir(os.path.join(self.project_path, dirname))):
375 dirs_to_create.append(dirname)
376 dirname = os.path.dirname(dirname)
377 for dir_to_create in reversed(dirs_to_create):
378 os.mkdir(os.path.join(self.project_path, dir_to_create))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000379 stdout.append(
380 self._check_output_svn(
381 ['add', dir_to_create, '--force'], credentials=False))
382 stdout.append('Created missing directory %s.' % dir_to_create)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000383
384 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000385 content = p.get()
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000386 with open(filepath, 'wb') as f:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000387 f.write(content)
388 stdout.append('Added binary file %d bytes.' % len(content))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000389 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000390 if p.source_filename:
391 if not p.is_new:
392 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000393 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000394 'File has a source filename specified but is not new')
395 # Copy the file first.
396 if os.path.isfile(filepath):
397 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000398 p, 'File exist but was about to be overwriten')
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000399 stdout.append(
400 self._check_output_svn(
401 ['copy', p.source_filename, p.filename]))
402 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000403 if p.diff_hunks:
maruel@chromium.orgec4a9182012-09-28 20:39:45 +0000404 cmd = [
405 'patch',
406 '-p%s' % p.patchlevel,
407 '--forward',
408 '--force',
409 '--no-backup-if-mismatch',
410 ]
groby@chromium.org23279942013-07-12 19:32:33 +0000411 env = os.environ.copy()
412 env['TMPDIR'] = tempfile.mkdtemp(prefix='crpatch')
413 try:
414 stdout.append(
415 subprocess2.check_output(
416 cmd,
417 stdin=p.get(False),
418 cwd=self.project_path,
419 timeout=GLOBAL_TIMEOUT,
420 env=env))
421 finally:
422 shutil.rmtree(env['TMPDIR'])
423
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000424 elif p.is_new and not os.path.exists(filepath):
425 # There is only a header. Just create the file if it doesn't
426 # exist.
427 open(filepath, 'w').close()
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000428 stdout.append('Created an empty file.')
maruel@chromium.org3da83172012-05-07 16:17:20 +0000429 if p.is_new and not p.source_filename:
430 # Do not run it if p.source_filename is defined, since svn copy was
431 # using above.
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000432 stdout.append(
433 self._check_output_svn(
434 ['add', p.filename, '--force'], credentials=False))
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000435 for name, value in p.svn_properties:
436 if value is None:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000437 stdout.append(
438 self._check_output_svn(
439 ['propdel', '--quiet', name, p.filename],
440 credentials=False))
441 stdout.append('Property %s deleted.' % name)
maruel@chromium.orgd7ca6162012-08-29 17:22:22 +0000442 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000443 stdout.append(
444 self._check_output_svn(
445 ['propset', name, value, p.filename], credentials=False))
446 stdout.append('Property %s=%s' % (name, value))
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000447 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000448 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000449 for value in values.split(';'):
450 if '=' not in value:
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000451 params = [value, '.']
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000452 else:
453 params = value.split('=', 1)
maruel@chromium.orge1a03762012-09-24 15:28:52 +0000454 if params[1] == '*':
455 # Works around crbug.com/150960 on Windows.
456 params[1] = '.'
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000457 stdout.append(
458 self._check_output_svn(
459 ['propset'] + params + [p.filename], credentials=False))
460 stdout.append('Property (auto) %s' % '='.join(params))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000461 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000462 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000463 if verbose:
464 print p.filename
465 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000466 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000467 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000468 except subprocess.CalledProcessError, e:
469 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000470 p,
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000471 'While running %s;\n%s%s' % (
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000472 ' '.join(e.cmd),
473 align_stdout(stdout),
474 align_stdout([getattr(e, 'stdout', '')])))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000475
476 def commit(self, commit_message, user):
477 logging.info('Committing patch for %s' % user)
478 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000479 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000480 handle, commit_filename = tempfile.mkstemp(text=True)
481 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000482 # Shouldn't assume default encoding is UTF-8. But really, if you are using
483 # anything else, you are living in another world.
484 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000485 os.close(handle)
486 # When committing, svn won't update the Revision metadata of the checkout,
487 # so if svn commit returns "Committed revision 3.", svn info will still
488 # return "Revision: 2". Since running svn update right after svn commit
489 # creates a race condition with other committers, this code _must_ parse
490 # the output of svn commit and use a regexp to grab the revision number.
491 # Note that "Committed revision N." is localized but subprocess2 forces
492 # LANGUAGE=en.
493 args = ['commit', '--file', commit_filename]
494 # realauthor is parsed by a server-side hook.
495 if user and user != self.commit_user:
496 args.extend(['--with-revprop', 'realauthor=%s' % user])
497 out = self._check_output_svn(args)
498 finally:
499 os.remove(commit_filename)
500 lines = filter(None, out.splitlines())
501 match = re.match(r'^Committed revision (\d+).$', lines[-1])
502 if not match:
503 raise PatchApplicationFailed(
504 None,
505 'Couldn\'t make sense out of svn commit message:\n' + out)
506 return int(match.group(1))
507
maruel@chromium.org51919772011-06-12 01:27:42 +0000508 def _revert(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000509 """Reverts local modifications or checks out if the directory is not
510 present. Use depot_tools's functionality to do this.
511 """
512 flags = ['--ignore-externals']
maruel@chromium.org51919772011-06-12 01:27:42 +0000513 if revision:
514 flags.extend(['--revision', str(revision)])
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000515 if os.path.isdir(self.project_path):
516 # This may remove any part (or all) of the checkout.
517 scm.SVN.Revert(self.project_path, no_ignore=True)
518
519 if os.path.isdir(self.project_path):
520 # Revive files that were deleted in scm.SVN.Revert().
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000521 self._check_call_svn(['update', '--force'] + flags,
522 timeout=FETCH_TIMEOUT)
maruel@chromium.org6e904b42012-12-19 14:21:14 +0000523 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000524 logging.info(
525 'Directory %s is not present, checking it out.' % self.project_path)
526 self._check_call_svn(
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000527 ['checkout', self.svn_url, self.project_path] + flags, cwd=None,
528 timeout=FETCH_TIMEOUT)
maruel@chromium.org51919772011-06-12 01:27:42 +0000529 return self._get_revision()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000530
maruel@chromium.org51919772011-06-12 01:27:42 +0000531 def _get_revision(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000532 out = self._check_output_svn(['info', '.'])
maruel@chromium.org51919772011-06-12 01:27:42 +0000533 revision = int(self._parse_svn_info(out, 'revision'))
534 if revision != self._last_seen_revision:
535 logging.info('Updated to revision %d' % revision)
536 self._last_seen_revision = revision
537 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000538
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000539 def revisions(self, rev1, rev2):
540 """Returns the number of actual commits, not just the difference between
541 numbers.
542 """
543 rev2 = rev2 or 'HEAD'
544 # Revision range is inclusive and ordering doesn't matter, they'll appear in
545 # the order specified.
546 try:
547 out = self._check_output_svn(
548 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
549 except subprocess.CalledProcessError:
550 return None
551 # Ignore the '----' lines.
552 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
553
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000554
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000555class GitCheckout(CheckoutBase):
556 """Manages a git checkout."""
557 def __init__(self, root_dir, project_name, remote_branch, git_url,
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000558 commit_user, post_processors=None):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000559 super(GitCheckout, self).__init__(root_dir, project_name, post_processors)
560 self.git_url = git_url
561 self.commit_user = commit_user
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000562 self.remote_branch = remote_branch
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000563 # The working branch where patches will be applied. It will track the
564 # remote branch.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000565 self.working_branch = 'working_branch'
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000566 # There is no reason to not hardcode origin.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000567 self.remote = 'origin'
568 # There is no reason to not hardcode master.
569 self.master_branch = 'master'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000570
maruel@chromium.org51919772011-06-12 01:27:42 +0000571 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000572 """Resets the git repository in a clean state.
573
574 Checks it out if not present and deletes the working branch.
575 """
agable@chromium.org7dc11442014-03-12 22:37:32 +0000576 assert self.remote_branch
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000577 assert self.git_url
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000578
579 if not os.path.isdir(self.project_path):
580 # Clone the repo if the directory is not present.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000581 logging.info(
582 'Checking out %s in %s', self.project_name, self.project_path)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000583 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000584 ['clone', self.git_url, '-b', self.remote_branch, self.project_path],
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000585 cwd=None, timeout=FETCH_TIMEOUT)
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000586 else:
587 # Throw away all uncommitted changes in the existing checkout.
588 self._check_call_git(['checkout', self.remote_branch])
589 self._check_call_git(
590 ['reset', '--hard', '--quiet',
591 '%s/%s' % (self.remote, self.remote_branch)])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000592
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000593 if revision:
594 try:
595 # Look if the commit hash already exist. If so, we can skip a
596 # 'git fetch' call.
597 revision = self._check_output_git(['rev-parse', revision])
598 except subprocess.CalledProcessError:
599 self._check_call_git(
600 ['fetch', self.remote, self.remote_branch, '--quiet'])
601 revision = self._check_output_git(['rev-parse', revision])
602 self._check_call_git(['checkout', '--force', '--quiet', revision])
603 else:
604 branches, active = self._branches()
605 if active != self.master_branch:
606 self._check_call_git(
607 ['checkout', '--force', '--quiet', self.master_branch])
608 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000609
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000610 if self.working_branch in branches:
611 self._call_git(['branch', '-D', self.working_branch])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000612 return self._get_head_commit_hash()
613
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000614 def _sync_remote_branch(self):
615 """Syncs the remote branch."""
616 # We do a 'git pull origin master:refs/remotes/origin/master' instead of
hinoka@google.comdabbea22014-04-21 23:58:11 +0000617 # 'git pull origin master' because from the manpage for git-pull:
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000618 # A parameter <ref> without a colon is equivalent to <ref>: when
619 # pulling/fetching, so it merges <ref> into the current branch without
620 # storing the remote branch anywhere locally.
621 remote_tracked_path = 'refs/remotes/%s/%s' % (
622 self.remote, self.remote_branch)
623 self._check_call_git(
624 ['pull', self.remote,
625 '%s:%s' % (self.remote_branch, remote_tracked_path),
626 '--quiet'])
627
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000628 def _get_head_commit_hash(self):
rmistry@google.com11145db2013-10-03 12:43:40 +0000629 """Gets the current revision (in unicode) from the local branch."""
630 return unicode(self._check_output_git(['rev-parse', 'HEAD']).strip())
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000631
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000632 def apply_patch(self, patches, post_processors=None, verbose=False):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000633 """Applies a patch on 'working_branch' and switches to it.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000634
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000635 The changes remain staged on the current branch.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000636
637 Ignores svn properties and raise an exception on unexpected ones.
638 """
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000639 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000640 # It this throws, the checkout is corrupted. Maybe worth deleting it and
641 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000642 if self.remote_branch:
643 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000644 ['checkout', '-b', self.working_branch, '-t', self.remote_branch,
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000645 '--quiet'])
646
maruel@chromium.org5e975632011-09-29 18:07:06 +0000647 for index, p in enumerate(patches):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000648 stdout = []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000649 try:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000650 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000651 if p.is_delete:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000652 if (not os.path.exists(filepath) and
maruel@chromium.org5e975632011-09-29 18:07:06 +0000653 any(p1.source_filename == p.filename for p1 in patches[0:index])):
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000654 # The file was already deleted if a prior patch with file rename
655 # was already processed because 'git apply' did it for us.
maruel@chromium.org5e975632011-09-29 18:07:06 +0000656 pass
657 else:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000658 stdout.append(self._check_output_git(['rm', p.filename]))
phajdan.jr@chromium.orgd9eb69e2014-06-05 20:33:37 +0000659 assert(not os.path.exists(filepath))
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000660 stdout.append('Deleted.')
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000661 else:
662 dirname = os.path.dirname(p.filename)
663 full_dir = os.path.join(self.project_path, dirname)
664 if dirname and not os.path.isdir(full_dir):
665 os.makedirs(full_dir)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000666 stdout.append('Created missing directory %s.' % dirname)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000667 if p.is_binary:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000668 content = p.get()
669 with open(filepath, 'wb') as f:
670 f.write(content)
671 stdout.append('Added binary file %d bytes' % len(content))
672 cmd = ['add', p.filename]
673 if verbose:
674 cmd.append('--verbose')
675 stdout.append(self._check_output_git(cmd))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000676 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000677 # No need to do anything special with p.is_new or if not
678 # p.diff_hunks. git apply manages all that already.
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000679 cmd = ['apply', '--index', '-p%s' % p.patchlevel]
680 if verbose:
681 cmd.append('--verbose')
682 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
hinoka@google.com64d819b2014-05-06 19:59:11 +0000683 for key, value in p.svn_properties:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000684 # Ignore some known auto-props flags through .subversion/config,
685 # bails out on the other ones.
686 # TODO(maruel): Read ~/.subversion/config and detect the rules that
687 # applies here to figure out if the property will be correctly
688 # handled.
hinoka@google.com64d819b2014-05-06 19:59:11 +0000689 stdout.append('Property %s=%s' % (key, value))
690 if not key in (
maruel@chromium.org9799a072012-01-11 00:26:25 +0000691 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000692 raise patch.UnsupportedPatchFormat(
693 p.filename,
694 'Cannot apply svn property %s to file %s.' % (
hinoka@google.com64d819b2014-05-06 19:59:11 +0000695 key, p.filename))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000696 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000697 post(self, p)
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000698 if verbose:
699 print p.filename
700 print align_stdout(stdout)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000701 except OSError, e:
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000702 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000703 except subprocess.CalledProcessError, e:
704 raise PatchApplicationFailed(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000705 p,
706 'While running %s;\n%s%s' % (
707 ' '.join(e.cmd),
708 align_stdout(stdout),
709 align_stdout([getattr(e, 'stdout', '')])))
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000710 found_files = self._check_output_git(
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000711 ['diff', '--ignore-submodules',
712 '--name-only', '--staged']).splitlines(False)
hinoka@chromium.orgdc6a1d02014-05-10 04:42:48 +0000713 if sorted(patches.filenames) != sorted(found_files):
714 extra_files = sorted(set(found_files) - set(patches.filenames))
715 unpatched_files = sorted(set(patches.filenames) - set(found_files))
716 if extra_files:
717 print 'Found extra files: %r' % (extra_files,)
718 if unpatched_files:
719 print 'Found unpatched files: %r' % (unpatched_files,)
720
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000721
722 def commit(self, commit_message, user):
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000723 """Commits, updates the commit message and pushes."""
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000724 # TODO(hinoka): CQ no longer uses this, I think its deprecated.
725 # Delete this.
rmistry@google.combb050f62013-10-03 16:53:54 +0000726 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000727 assert isinstance(commit_message, unicode)
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000728 current_branch = self._check_output_git(
729 ['rev-parse', '--abbrev-ref', 'HEAD']).strip()
730 assert current_branch == self.working_branch
hinoka@google.comdabbea22014-04-21 23:58:11 +0000731
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000732 commit_cmd = ['commit', '-m', commit_message]
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000733 if user and user != self.commit_user:
734 # We do not have the first or last name of the user, grab the username
735 # from the email and call it the original author's name.
736 # TODO(rmistry): Do not need the below if user is already in
737 # "Name <email>" format.
738 name = user.split('@')[0]
739 commit_cmd.extend(['--author', '%s <%s>' % (name, user)])
740 self._check_call_git(commit_cmd)
741
742 # Push to the remote repository.
743 self._check_call_git(
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000744 ['push', 'origin', '%s:%s' % (self.working_branch, self.remote_branch),
agable@chromium.org39262282014-03-19 21:07:38 +0000745 '--quiet'])
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000746 # Get the revision after the push.
747 revision = self._get_head_commit_hash()
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000748 # Switch back to the remote_branch and sync it.
749 self._check_call_git(['checkout', self.remote_branch])
750 self._sync_remote_branch()
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000751 # Delete the working branch since we are done with it.
752 self._check_call_git(['branch', '-D', self.working_branch])
753
754 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000755
756 def _check_call_git(self, args, **kwargs):
757 kwargs.setdefault('cwd', self.project_path)
758 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000759 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000760 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000761
762 def _call_git(self, args, **kwargs):
763 """Like check_call but doesn't throw on failure."""
764 kwargs.setdefault('cwd', self.project_path)
765 kwargs.setdefault('stdout', self.VOID)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000766 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000767 return subprocess2.call(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000768
769 def _check_output_git(self, args, **kwargs):
770 kwargs.setdefault('cwd', self.project_path)
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000771 kwargs.setdefault('timeout', GLOBAL_TIMEOUT)
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000772 return subprocess2.check_output(
maruel@chromium.org44b21b92012-11-08 19:37:08 +0000773 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000774
775 def _branches(self):
776 """Returns the list of branches and the active one."""
777 out = self._check_output_git(['branch']).splitlines(False)
778 branches = [l[2:] for l in out]
779 active = None
780 for l in out:
781 if l.startswith('*'):
782 active = l[2:]
783 break
784 return branches, active
785
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000786 def revisions(self, rev1, rev2):
787 """Returns the number of actual commits between both hash."""
788 self._fetch_remote()
789
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000790 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000791 # Revision range is ]rev1, rev2] and ordering matters.
792 try:
793 out = self._check_output_git(
794 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
795 except subprocess.CalledProcessError:
796 return None
797 return len(out.splitlines())
798
799 def _fetch_remote(self):
800 """Fetches the remote without rebasing."""
rmistry@google.com3b5efdf2013-09-05 11:59:40 +0000801 # git fetch is always verbose even with -q, so redirect its output.
agable@chromium.org7e8c19d2014-03-19 16:47:37 +0000802 self._check_output_git(['fetch', self.remote, self.remote_branch],
csharp@chromium.org9af0a112013-03-20 20:21:35 +0000803 timeout=FETCH_TIMEOUT)
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000804
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000805
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000806class ReadOnlyCheckout(object):
807 """Converts a checkout into a read-only one."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000808 def __init__(self, checkout, post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000809 super(ReadOnlyCheckout, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000810 self.checkout = checkout
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000811 self.post_processors = (post_processors or []) + (
812 self.checkout.post_processors or [])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000813
maruel@chromium.org51919772011-06-12 01:27:42 +0000814 def prepare(self, revision):
815 return self.checkout.prepare(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000816
817 def get_settings(self, key):
818 return self.checkout.get_settings(key)
819
hinoka@chromium.orgc4396a12014-05-10 02:19:27 +0000820 def apply_patch(self, patches, post_processors=None, verbose=False):
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000821 return self.checkout.apply_patch(
maruel@chromium.org4dd9f722012-10-01 16:23:03 +0000822 patches, post_processors or self.post_processors, verbose)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000823
824 def commit(self, message, user): # pylint: disable=R0201
825 logging.info('Would have committed for %s with message: %s' % (
826 user, message))
827 return 'FAKE'
828
maruel@chromium.orgbc32ad12012-07-26 13:22:47 +0000829 def revisions(self, rev1, rev2):
830 return self.checkout.revisions(rev1, rev2)
831
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000832 @property
833 def project_name(self):
834 return self.checkout.project_name
835
836 @property
837 def project_path(self):
838 return self.checkout.project_path