blob: ab449b4dad5346f75fac99ccbcbce315cc084290 [file] [log] [blame]
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00001# coding=utf8
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# 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
10from __future__ import with_statement
11import ConfigParser
12import fnmatch
13import logging
14import os
15import re
16import subprocess
17import sys
18import tempfile
19
20import patch
21import scm
22import subprocess2
23
24
25def get_code_review_setting(path, key,
26 codereview_settings_file='codereview.settings'):
27 """Parses codereview.settings and return the value for the key if present.
28
29 Don't cache the values in case the file is changed."""
30 # TODO(maruel): Do not duplicate code.
31 settings = {}
32 try:
33 settings_file = open(os.path.join(path, codereview_settings_file), 'r')
34 try:
35 for line in settings_file.readlines():
36 if not line or line.startswith('#'):
37 continue
38 if not ':' in line:
39 # Invalid file.
40 return None
41 k, v = line.split(':', 1)
42 settings[k.strip()] = v.strip()
43 finally:
44 settings_file.close()
45 except OSError:
46 return None
47 return settings.get(key, None)
48
49
50class PatchApplicationFailed(Exception):
51 """Patch failed to be applied."""
52 def __init__(self, filename, status):
53 super(PatchApplicationFailed, self).__init__(filename, status)
54 self.filename = filename
55 self.status = status
56
57
58class CheckoutBase(object):
59 # Set to None to have verbose output.
60 VOID = subprocess2.VOID
61
62 def __init__(self, root_dir, project_name):
63 self.root_dir = root_dir
64 self.project_name = project_name
65 self.project_path = os.path.join(self.root_dir, self.project_name)
66 # Only used for logging purposes.
67 self._last_seen_revision = None
68 assert self.root_dir
69 assert self.project_name
70 assert self.project_path
71
72 def get_settings(self, key):
73 return get_code_review_setting(self.project_path, key)
74
75 def prepare(self):
76 """Checks out a clean copy of the tree and removes any local modification.
77
78 This function shouldn't throw unless the remote repository is inaccessible,
79 there is no free disk space or hard issues like that.
80 """
81 raise NotImplementedError()
82
maruel@chromium.org8a1396c2011-04-22 00:14:24 +000083 def apply_patch(self, patches, post_processor=None):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000084 """Applies a patch and returns the list of modified files.
85
86 This function should throw patch.UnsupportedPatchFormat or
87 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +000088
89 Args:
90 patches: patch.PatchSet object.
91 post_processor: list of lambda(checkout, patches) to call on each of the
92 modified files.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000093 """
94 raise NotImplementedError()
95
96 def commit(self, commit_message, user):
97 """Commits the patch upstream, while impersonating 'user'."""
98 raise NotImplementedError()
99
100
101class RawCheckout(CheckoutBase):
102 """Used to apply a patch locally without any intent to commit it.
103
104 To be used by the try server.
105 """
106 def prepare(self):
107 """Stubbed out."""
108 pass
109
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000110 def apply_patch(self, patches, post_processor=None):
111 """Ignores svn properties."""
112 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000113 for p in patches:
114 try:
115 stdout = ''
116 filename = os.path.join(self.project_path, p.filename)
117 if p.is_delete:
118 os.remove(filename)
119 else:
120 dirname = os.path.dirname(p.filename)
121 full_dir = os.path.join(self.project_path, dirname)
122 if dirname and not os.path.isdir(full_dir):
123 os.makedirs(full_dir)
124 if p.is_binary:
125 with open(os.path.join(filename), 'wb') as f:
126 f.write(p.get())
127 else:
128 stdout = subprocess2.check_output(
129 ['patch', '-p%s' % p.patchlevel],
130 stdin=p.get(),
131 cwd=self.project_path)
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000132 for post in post_processor:
133 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000134 except OSError, e:
135 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
136 except subprocess.CalledProcessError, e:
137 raise PatchApplicationFailed(
138 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
139
140 def commit(self, commit_message, user):
141 """Stubbed out."""
142 raise NotImplementedError('RawCheckout can\'t commit')
143
144
145class SvnConfig(object):
146 """Parses a svn configuration file."""
147 def __init__(self, svn_config_dir=None):
148 self.svn_config_dir = svn_config_dir
149 self.default = not bool(self.svn_config_dir)
150 if not self.svn_config_dir:
151 if sys.platform == 'win32':
152 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
153 else:
154 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
155 svn_config_file = os.path.join(self.svn_config_dir, 'config')
156 parser = ConfigParser.SafeConfigParser()
157 if os.path.isfile(svn_config_file):
158 parser.read(svn_config_file)
159 else:
160 parser.add_section('auto-props')
161 self.auto_props = dict(parser.items('auto-props'))
162
163
164class SvnMixIn(object):
165 """MixIn class to add svn commands common to both svn and git-svn clients."""
166 # These members need to be set by the subclass.
167 commit_user = None
168 commit_pwd = None
169 svn_url = None
170 project_path = None
171 # Override at class level when necessary. If used, --non-interactive is
172 # implied.
173 svn_config = SvnConfig()
174 # Set to True when non-interactivity is necessary but a custom subversion
175 # configuration directory is not necessary.
176 non_interactive = False
177
178 def _add_svn_flags(self, args, non_interactive):
179 args = ['svn'] + args
180 if not self.svn_config.default:
181 args.extend(['--config-dir', self.svn_config.svn_config_dir])
182 if not self.svn_config.default or self.non_interactive or non_interactive:
183 args.append('--non-interactive')
184 if self.commit_user:
185 args.extend(['--username', self.commit_user])
186 if self.commit_pwd:
187 args.extend(['--password', self.commit_pwd])
188 return args
189
190 def _check_call_svn(self, args, **kwargs):
191 """Runs svn and throws an exception if the command failed."""
192 kwargs.setdefault('cwd', self.project_path)
193 kwargs.setdefault('stdout', self.VOID)
194 return subprocess2.check_call(self._add_svn_flags(args, False), **kwargs)
195
196 def _check_output_svn(self, args, **kwargs):
197 """Runs svn and throws an exception if the command failed.
198
199 Returns the output.
200 """
201 kwargs.setdefault('cwd', self.project_path)
202 return subprocess2.check_output(self._add_svn_flags(args, True), **kwargs)
203
204 @staticmethod
205 def _parse_svn_info(output, key):
206 """Returns value for key from svn info output.
207
208 Case insensitive.
209 """
210 values = {}
211 key = key.lower()
212 for line in output.splitlines(False):
213 if not line:
214 continue
215 k, v = line.split(':', 1)
216 k = k.strip().lower()
217 v = v.strip()
218 assert not k in values
219 values[k] = v
220 return values.get(key, None)
221
222
223class SvnCheckout(CheckoutBase, SvnMixIn):
224 """Manages a subversion checkout."""
225 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url):
226 super(SvnCheckout, self).__init__(root_dir, project_name)
227 self.commit_user = commit_user
228 self.commit_pwd = commit_pwd
229 self.svn_url = svn_url
230 assert bool(self.commit_user) >= bool(self.commit_pwd)
231 assert self.svn_url
232
233 def prepare(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000234 # Will checkout if the directory is not present.
235 if not os.path.isdir(self.project_path):
236 logging.info('Checking out %s in %s' %
237 (self.project_name, self.project_path))
238 revision = self._revert()
239 if revision != self._last_seen_revision:
240 logging.info('Updated at revision %d' % revision)
241 self._last_seen_revision = revision
242 return revision
243
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000244 def apply_patch(self, patches, post_processor=None):
245 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000246 for p in patches:
247 try:
248 stdout = ''
249 if p.is_delete:
250 stdout += self._check_output_svn(['delete', p.filename, '--force'])
251 else:
252 new = not os.path.exists(p.filename)
253
254 # svn add while creating directories otherwise svn add on the
255 # contained files will silently fail.
256 # First, find the root directory that exists.
257 dirname = os.path.dirname(p.filename)
258 dirs_to_create = []
259 while (dirname and
260 not os.path.isdir(os.path.join(self.project_path, dirname))):
261 dirs_to_create.append(dirname)
262 dirname = os.path.dirname(dirname)
263 for dir_to_create in reversed(dirs_to_create):
264 os.mkdir(os.path.join(self.project_path, dir_to_create))
265 stdout += self._check_output_svn(
266 ['add', dir_to_create, '--force'])
267
268 if p.is_binary:
269 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
270 f.write(p.get())
271 else:
272 cmd = ['patch', '-p%s' % p.patchlevel, '--forward', '--force']
273 stdout += subprocess2.check_output(
274 cmd, stdin=p.get(), cwd=self.project_path)
275 if new:
276 stdout += self._check_output_svn(['add', p.filename, '--force'])
277 for prop in p.svn_properties:
278 stdout += self._check_output_svn(
279 ['propset', prop[0], prop[1], p.filename])
280 for prop, value in self.svn_config.auto_props.iteritems():
281 if fnmatch.fnmatch(p.filename, prop):
282 stdout += self._check_output_svn(
283 ['propset'] + value.split('=', 1) + [p.filename])
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000284 for post in post_processor:
285 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000286 except OSError, e:
287 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
288 except subprocess.CalledProcessError, e:
289 raise PatchApplicationFailed(
290 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', '')))
291
292 def commit(self, commit_message, user):
293 logging.info('Committing patch for %s' % user)
294 assert self.commit_user
295 handle, commit_filename = tempfile.mkstemp(text=True)
296 try:
297 os.write(handle, commit_message)
298 os.close(handle)
299 # When committing, svn won't update the Revision metadata of the checkout,
300 # so if svn commit returns "Committed revision 3.", svn info will still
301 # return "Revision: 2". Since running svn update right after svn commit
302 # creates a race condition with other committers, this code _must_ parse
303 # the output of svn commit and use a regexp to grab the revision number.
304 # Note that "Committed revision N." is localized but subprocess2 forces
305 # LANGUAGE=en.
306 args = ['commit', '--file', commit_filename]
307 # realauthor is parsed by a server-side hook.
308 if user and user != self.commit_user:
309 args.extend(['--with-revprop', 'realauthor=%s' % user])
310 out = self._check_output_svn(args)
311 finally:
312 os.remove(commit_filename)
313 lines = filter(None, out.splitlines())
314 match = re.match(r'^Committed revision (\d+).$', lines[-1])
315 if not match:
316 raise PatchApplicationFailed(
317 None,
318 'Couldn\'t make sense out of svn commit message:\n' + out)
319 return int(match.group(1))
320
321 def _revert(self):
322 """Reverts local modifications or checks out if the directory is not
323 present. Use depot_tools's functionality to do this.
324 """
325 flags = ['--ignore-externals']
326 if not os.path.isdir(self.project_path):
327 logging.info(
328 'Directory %s is not present, checking it out.' % self.project_path)
329 self._check_call_svn(
330 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
331 else:
332 scm.SVN.Revert(self.project_path)
333 # Revive files that were deleted in scm.SVN.Revert().
334 self._check_call_svn(['update', '--force'] + flags)
335
336 out = self._check_output_svn(['info', '.'])
337 return int(self._parse_svn_info(out, 'revision'))
338
339
340class GitCheckoutBase(CheckoutBase):
341 """Base class for git checkout. Not to be used as-is."""
342 def __init__(self, root_dir, project_name, remote_branch):
343 super(GitCheckoutBase, self).__init__(root_dir, project_name)
344 # There is no reason to not hardcode it.
345 self.remote = 'origin'
346 self.remote_branch = remote_branch
347 self.working_branch = 'working_branch'
348 assert self.remote_branch
349
350 def prepare(self):
351 """Resets the git repository in a clean state.
352
353 Checks it out if not present and deletes the working branch.
354 """
355 assert os.path.isdir(self.project_path)
356 self._check_call_git(['reset', '--hard', '--quiet'])
357 branches, active = self._branches()
358 if active != 'master':
359 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
360 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
361 if self.working_branch in branches:
362 self._call_git(['branch', '-D', self.working_branch])
363
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000364 def apply_patch(self, patches, post_processor=None):
365 """Applies a patch on 'working_branch' and switch to it.
366
367 Also commits the changes on the local branch.
368
369 Ignores svn properties and raise an exception on unexpected ones.
370 """
371 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000372 # It this throws, the checkout is corrupted. Maybe worth deleting it and
373 # trying again?
374 self._check_call_git(
375 ['checkout', '-b', self.working_branch,
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000376 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000377 for p in patches:
378 try:
379 stdout = ''
380 if p.is_delete:
381 stdout += self._check_output_git(['rm', p.filename])
382 else:
383 dirname = os.path.dirname(p.filename)
384 full_dir = os.path.join(self.project_path, dirname)
385 if dirname and not os.path.isdir(full_dir):
386 os.makedirs(full_dir)
387 if p.is_binary:
388 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
389 f.write(p.get())
390 stdout += self._check_output_git(['add', p.filename])
391 else:
392 stdout += self._check_output_git(
393 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get())
394 for prop in p.svn_properties:
395 # Ignore some known auto-props flags through .subversion/config,
396 # bails out on the other ones.
397 # TODO(maruel): Read ~/.subversion/config and detect the rules that
398 # applies here to figure out if the property will be correctly
399 # handled.
400 if not prop[0] in ('svn:eol-style', 'svn:executable'):
401 raise patch.UnsupportedPatchFormat(
402 p.filename,
403 'Cannot apply svn property %s to file %s.' % (
404 prop[0], p.filename))
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000405 for post in post_processor:
406 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000407 except OSError, e:
408 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
409 except subprocess.CalledProcessError, e:
410 raise PatchApplicationFailed(
411 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
412 # Once all the patches are processed and added to the index, commit the
413 # index.
414 self._check_call_git(['commit', '-m', 'Committed patch'])
415 # TODO(maruel): Weirdly enough they don't match, need to investigate.
416 #found_files = self._check_output_git(
417 # ['diff', 'master', '--name-only']).splitlines(False)
418 #assert sorted(patches.filenames) == sorted(found_files), (
419 # sorted(out), sorted(found_files))
420
421 def commit(self, commit_message, user):
422 """Updates the commit message.
423
424 Subclass needs to dcommit or push.
425 """
426 self._check_call_git(['commit', '--amend', '-m', commit_message])
427 return self._check_output_git(['rev-parse', 'HEAD']).strip()
428
429 def _check_call_git(self, args, **kwargs):
430 kwargs.setdefault('cwd', self.project_path)
431 kwargs.setdefault('stdout', self.VOID)
432 return subprocess2.check_call(['git'] + args, **kwargs)
433
434 def _call_git(self, args, **kwargs):
435 """Like check_call but doesn't throw on failure."""
436 kwargs.setdefault('cwd', self.project_path)
437 kwargs.setdefault('stdout', self.VOID)
438 return subprocess2.call(['git'] + args, **kwargs)
439
440 def _check_output_git(self, args, **kwargs):
441 kwargs.setdefault('cwd', self.project_path)
442 return subprocess2.check_output(['git'] + args, **kwargs)
443
444 def _branches(self):
445 """Returns the list of branches and the active one."""
446 out = self._check_output_git(['branch']).splitlines(False)
447 branches = [l[2:] for l in out]
448 active = None
449 for l in out:
450 if l.startswith('*'):
451 active = l[2:]
452 break
453 return branches, active
454
455
456class GitSvnCheckoutBase(GitCheckoutBase, SvnMixIn):
457 """Base class for git-svn checkout. Not to be used as-is."""
458 def __init__(self,
459 root_dir, project_name, remote_branch,
460 commit_user, commit_pwd,
461 svn_url, trunk):
462 """trunk is optional."""
463 super(GitSvnCheckoutBase, self).__init__(
464 root_dir, project_name + '.git', remote_branch)
465 self.commit_user = commit_user
466 self.commit_pwd = commit_pwd
467 # svn_url in this case is the root of the svn repository.
468 self.svn_url = svn_url
469 self.trunk = trunk
470 assert bool(self.commit_user) >= bool(self.commit_pwd)
471 assert self.svn_url
472 assert self.trunk
473 self._cache_svn_auth()
474
475 def prepare(self):
476 """Resets the git repository in a clean state."""
477 self._check_call_git(['reset', '--hard', '--quiet'])
478 branches, active = self._branches()
479 if active != 'master':
480 if not 'master' in branches:
481 self._check_call_git(
482 ['checkout', '--quiet', '-b', 'master',
483 '%s/%s' % (self.remote, self.remote_branch)])
484 else:
485 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
486 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it.
487 self._check_call_git_svn(['fetch', '--quiet', '--quiet'])
488 self._check_call_git(
489 ['rebase', '--quiet', '--quiet',
490 '%s/%s' % (self.remote, self.remote_branch)])
491 if self.working_branch in branches:
492 self._call_git(['branch', '-D', self.working_branch])
493 return int(self._git_svn_info('revision'))
494
495 def _git_svn_info(self, key):
496 """Calls git svn info. This doesn't support nor need --config-dir."""
497 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key)
498
499 def commit(self, commit_message, user):
500 """Commits a patch."""
501 logging.info('Committing patch for %s' % user)
502 # Fix the commit message and author. It returns the git hash, which we
503 # ignore unless it's None.
504 if not super(GitSvnCheckoutBase, self).commit(commit_message, user):
505 return None
506 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and
507 # doesn't support --with-revprop.
508 # Either learn perl and upstream or suck it.
509 kwargs = {}
510 if self.commit_pwd:
511 kwargs['stdin'] = self.commit_pwd + '\n'
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000512 kwargs['stderr'] = subprocess2.STDOUT
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000513 self._check_call_git_svn(
514 ['dcommit', '--rmdir', '--find-copies-harder',
515 '--username', self.commit_user],
516 **kwargs)
517 revision = int(self._git_svn_info('revision'))
518 return revision
519
520 def _cache_svn_auth(self):
521 """Caches the svn credentials. It is necessary since git-svn doesn't prompt
522 for it."""
523 if not self.commit_user or not self.commit_pwd:
524 return
525 # Use capture to lower noise in logs.
526 self._check_output_svn(['ls', self.svn_url], cwd=None)
527
528 def _check_call_git_svn(self, args, **kwargs):
529 """Handles svn authentication while calling git svn."""
530 args = ['svn'] + args
531 if not self.svn_config.default:
532 args.extend(['--config-dir', self.svn_config.svn_config_dir])
533 return self._check_call_git(args, **kwargs)
534
535 def _get_revision(self):
536 revision = int(self._git_svn_info('revision'))
537 if revision != self._last_seen_revision:
538 logging.info('Updated at revision %d' % revision)
539 self._last_seen_revision = revision
540 return revision
541
542
543class GitSvnPremadeCheckout(GitSvnCheckoutBase):
544 """Manages a git-svn clone made out from an initial git-svn seed.
545
546 This class is very similar to GitSvnCheckout but is faster to bootstrap
547 because it starts right off with an existing git-svn clone.
548 """
549 def __init__(self,
550 root_dir, project_name, remote_branch,
551 commit_user, commit_pwd,
552 svn_url, trunk, git_url):
553 super(GitSvnPremadeCheckout, self).__init__(
554 root_dir, project_name, remote_branch,
555 commit_user, commit_pwd,
556 svn_url, trunk)
557 self.git_url = git_url
558 assert self.git_url
559
560 def prepare(self):
561 """Creates the initial checkout for the repo."""
562 if not os.path.isdir(self.project_path):
563 logging.info('Checking out %s in %s' %
564 (self.project_name, self.project_path))
565 assert self.remote == 'origin'
566 # self.project_path doesn't exist yet.
567 self._check_call_git(
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000568 ['clone', self.git_url, self.project_name, '--quiet'],
569 cwd=self.root_dir,
570 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000571 try:
572 configured_svn_url = self._check_output_git(
573 ['config', 'svn-remote.svn.url']).strip()
574 except subprocess.CalledProcessError:
575 configured_svn_url = ''
576
577 if configured_svn_url.strip() != self.svn_url:
578 self._check_call_git_svn(
579 ['init',
580 '--prefix', self.remote + '/',
581 '-T', self.trunk,
582 self.svn_url])
583 self._check_call_git_svn(['fetch'])
584 super(GitSvnPremadeCheckout, self).prepare()
585 return self._get_revision()
586
587
588class GitSvnCheckout(GitSvnCheckoutBase):
589 """Manages a git-svn clone.
590
591 Using git-svn hides some of the complexity of using a svn checkout.
592 """
593 def __init__(self,
594 root_dir, project_name,
595 commit_user, commit_pwd,
596 svn_url, trunk):
597 super(GitSvnCheckout, self).__init__(
598 root_dir, project_name, 'trunk',
599 commit_user, commit_pwd,
600 svn_url, trunk)
601
602 def prepare(self):
603 """Creates the initial checkout for the repo."""
604 if not os.path.isdir(self.project_path):
605 logging.info('Checking out %s in %s' %
606 (self.project_name, self.project_path))
607 # TODO: Create a shallow clone.
608 # self.project_path doesn't exist yet.
609 self._check_call_git_svn(
610 ['clone',
611 '--prefix', self.remote + '/',
612 '-T', self.trunk,
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000613 self.svn_url, self.project_path,
614 '--quiet'],
615 cwd=self.root_dir,
616 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000617 super(GitSvnCheckout, self).prepare()
618 return self._get_revision()
619
620
621class ReadOnlyCheckout(object):
622 """Converts a checkout into a read-only one."""
623 def __init__(self, checkout):
624 self.checkout = checkout
625
626 def prepare(self):
627 return self.checkout.prepare()
628
629 def get_settings(self, key):
630 return self.checkout.get_settings(key)
631
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000632 def apply_patch(self, patches, post_processor=None):
633 return self.checkout.apply_patch(patches, post_processor)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000634
635 def commit(self, message, user): # pylint: disable=R0201
636 logging.info('Would have committed for %s with message: %s' % (
637 user, message))
638 return 'FAKE'
639
640 @property
641 def project_name(self):
642 return self.checkout.project_name
643
644 @property
645 def project_path(self):
646 return self.checkout.project_path