blob: 38c269abe1d05e41bdc2fffb8807882a578d164d [file] [log] [blame]
[email protected]dfaecd22011-04-21 00:33:311# 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
[email protected]8a1396c2011-04-22 00:14:2483 def apply_patch(self, patches, post_processor=None):
[email protected]dfaecd22011-04-21 00:33:3184 """Applies a patch and returns the list of modified files.
85
86 This function should throw patch.UnsupportedPatchFormat or
87 PatchApplicationFailed when relevant.
[email protected]8a1396c2011-04-22 00:14:2488
89 Args:
90 patches: patch.PatchSet object.
91 post_processor: list of lambda(checkout, patches) to call on each of the
92 modified files.
[email protected]dfaecd22011-04-21 00:33:3193 """
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
[email protected]8a1396c2011-04-22 00:14:24110 def apply_patch(self, patches, post_processor=None):
111 """Ignores svn properties."""
112 post_processor = post_processor or []
[email protected]dfaecd22011-04-21 00:33:31113 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)
[email protected]8a1396c2011-04-22 00:14:24132 for post in post_processor:
133 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31134 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)
[email protected]0bcd1d32011-04-26 15:55:49194 return subprocess2.check_call_out(
195 self._add_svn_flags(args, False), **kwargs)
[email protected]dfaecd22011-04-21 00:33:31196
197 def _check_output_svn(self, args, **kwargs):
198 """Runs svn and throws an exception if the command failed.
199
200 Returns the output.
201 """
202 kwargs.setdefault('cwd', self.project_path)
203 return subprocess2.check_output(self._add_svn_flags(args, True), **kwargs)
204
205 @staticmethod
206 def _parse_svn_info(output, key):
207 """Returns value for key from svn info output.
208
209 Case insensitive.
210 """
211 values = {}
212 key = key.lower()
213 for line in output.splitlines(False):
214 if not line:
215 continue
216 k, v = line.split(':', 1)
217 k = k.strip().lower()
218 v = v.strip()
219 assert not k in values
220 values[k] = v
221 return values.get(key, None)
222
223
224class SvnCheckout(CheckoutBase, SvnMixIn):
225 """Manages a subversion checkout."""
226 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url):
227 super(SvnCheckout, self).__init__(root_dir, project_name)
228 self.commit_user = commit_user
229 self.commit_pwd = commit_pwd
230 self.svn_url = svn_url
231 assert bool(self.commit_user) >= bool(self.commit_pwd)
232 assert self.svn_url
233
234 def prepare(self):
[email protected]dfaecd22011-04-21 00:33:31235 # Will checkout if the directory is not present.
236 if not os.path.isdir(self.project_path):
237 logging.info('Checking out %s in %s' %
238 (self.project_name, self.project_path))
239 revision = self._revert()
240 if revision != self._last_seen_revision:
241 logging.info('Updated at revision %d' % revision)
242 self._last_seen_revision = revision
243 return revision
244
[email protected]8a1396c2011-04-22 00:14:24245 def apply_patch(self, patches, post_processor=None):
246 post_processor = post_processor or []
[email protected]dfaecd22011-04-21 00:33:31247 for p in patches:
248 try:
249 stdout = ''
250 if p.is_delete:
251 stdout += self._check_output_svn(['delete', p.filename, '--force'])
252 else:
253 new = not os.path.exists(p.filename)
254
255 # svn add while creating directories otherwise svn add on the
256 # contained files will silently fail.
257 # First, find the root directory that exists.
258 dirname = os.path.dirname(p.filename)
259 dirs_to_create = []
260 while (dirname and
261 not os.path.isdir(os.path.join(self.project_path, dirname))):
262 dirs_to_create.append(dirname)
263 dirname = os.path.dirname(dirname)
264 for dir_to_create in reversed(dirs_to_create):
265 os.mkdir(os.path.join(self.project_path, dir_to_create))
266 stdout += self._check_output_svn(
267 ['add', dir_to_create, '--force'])
268
269 if p.is_binary:
270 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
271 f.write(p.get())
272 else:
273 cmd = ['patch', '-p%s' % p.patchlevel, '--forward', '--force']
274 stdout += subprocess2.check_output(
275 cmd, stdin=p.get(), cwd=self.project_path)
276 if new:
277 stdout += self._check_output_svn(['add', p.filename, '--force'])
278 for prop in p.svn_properties:
279 stdout += self._check_output_svn(
280 ['propset', prop[0], prop[1], p.filename])
281 for prop, value in self.svn_config.auto_props.iteritems():
282 if fnmatch.fnmatch(p.filename, prop):
283 stdout += self._check_output_svn(
284 ['propset'] + value.split('=', 1) + [p.filename])
[email protected]8a1396c2011-04-22 00:14:24285 for post in post_processor:
286 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31287 except OSError, e:
288 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
289 except subprocess.CalledProcessError, e:
290 raise PatchApplicationFailed(
291 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', '')))
292
293 def commit(self, commit_message, user):
294 logging.info('Committing patch for %s' % user)
295 assert self.commit_user
296 handle, commit_filename = tempfile.mkstemp(text=True)
297 try:
298 os.write(handle, commit_message)
299 os.close(handle)
300 # When committing, svn won't update the Revision metadata of the checkout,
301 # so if svn commit returns "Committed revision 3.", svn info will still
302 # return "Revision: 2". Since running svn update right after svn commit
303 # creates a race condition with other committers, this code _must_ parse
304 # the output of svn commit and use a regexp to grab the revision number.
305 # Note that "Committed revision N." is localized but subprocess2 forces
306 # LANGUAGE=en.
307 args = ['commit', '--file', commit_filename]
308 # realauthor is parsed by a server-side hook.
309 if user and user != self.commit_user:
310 args.extend(['--with-revprop', 'realauthor=%s' % user])
311 out = self._check_output_svn(args)
312 finally:
313 os.remove(commit_filename)
314 lines = filter(None, out.splitlines())
315 match = re.match(r'^Committed revision (\d+).$', lines[-1])
316 if not match:
317 raise PatchApplicationFailed(
318 None,
319 'Couldn\'t make sense out of svn commit message:\n' + out)
320 return int(match.group(1))
321
322 def _revert(self):
323 """Reverts local modifications or checks out if the directory is not
324 present. Use depot_tools's functionality to do this.
325 """
326 flags = ['--ignore-externals']
327 if not os.path.isdir(self.project_path):
328 logging.info(
329 'Directory %s is not present, checking it out.' % self.project_path)
330 self._check_call_svn(
331 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
332 else:
333 scm.SVN.Revert(self.project_path)
334 # Revive files that were deleted in scm.SVN.Revert().
335 self._check_call_svn(['update', '--force'] + flags)
336
337 out = self._check_output_svn(['info', '.'])
338 return int(self._parse_svn_info(out, 'revision'))
339
340
341class GitCheckoutBase(CheckoutBase):
342 """Base class for git checkout. Not to be used as-is."""
343 def __init__(self, root_dir, project_name, remote_branch):
344 super(GitCheckoutBase, self).__init__(root_dir, project_name)
345 # There is no reason to not hardcode it.
346 self.remote = 'origin'
347 self.remote_branch = remote_branch
348 self.working_branch = 'working_branch'
349 assert self.remote_branch
350
351 def prepare(self):
352 """Resets the git repository in a clean state.
353
354 Checks it out if not present and deletes the working branch.
355 """
356 assert os.path.isdir(self.project_path)
357 self._check_call_git(['reset', '--hard', '--quiet'])
358 branches, active = self._branches()
359 if active != 'master':
360 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
361 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
362 if self.working_branch in branches:
363 self._call_git(['branch', '-D', self.working_branch])
364
[email protected]8a1396c2011-04-22 00:14:24365 def apply_patch(self, patches, post_processor=None):
366 """Applies a patch on 'working_branch' and switch to it.
367
368 Also commits the changes on the local branch.
369
370 Ignores svn properties and raise an exception on unexpected ones.
371 """
372 post_processor = post_processor or []
[email protected]dfaecd22011-04-21 00:33:31373 # It this throws, the checkout is corrupted. Maybe worth deleting it and
374 # trying again?
375 self._check_call_git(
376 ['checkout', '-b', self.working_branch,
[email protected]8a1396c2011-04-22 00:14:24377 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
[email protected]dfaecd22011-04-21 00:33:31378 for p in patches:
379 try:
380 stdout = ''
381 if p.is_delete:
382 stdout += self._check_output_git(['rm', p.filename])
383 else:
384 dirname = os.path.dirname(p.filename)
385 full_dir = os.path.join(self.project_path, dirname)
386 if dirname and not os.path.isdir(full_dir):
387 os.makedirs(full_dir)
388 if p.is_binary:
389 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
390 f.write(p.get())
391 stdout += self._check_output_git(['add', p.filename])
392 else:
393 stdout += self._check_output_git(
394 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get())
395 for prop in p.svn_properties:
396 # Ignore some known auto-props flags through .subversion/config,
397 # bails out on the other ones.
398 # TODO(maruel): Read ~/.subversion/config and detect the rules that
399 # applies here to figure out if the property will be correctly
400 # handled.
401 if not prop[0] in ('svn:eol-style', 'svn:executable'):
402 raise patch.UnsupportedPatchFormat(
403 p.filename,
404 'Cannot apply svn property %s to file %s.' % (
405 prop[0], p.filename))
[email protected]8a1396c2011-04-22 00:14:24406 for post in post_processor:
407 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31408 except OSError, e:
409 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
410 except subprocess.CalledProcessError, e:
411 raise PatchApplicationFailed(
412 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
413 # Once all the patches are processed and added to the index, commit the
414 # index.
415 self._check_call_git(['commit', '-m', 'Committed patch'])
416 # TODO(maruel): Weirdly enough they don't match, need to investigate.
417 #found_files = self._check_output_git(
418 # ['diff', 'master', '--name-only']).splitlines(False)
419 #assert sorted(patches.filenames) == sorted(found_files), (
420 # sorted(out), sorted(found_files))
421
422 def commit(self, commit_message, user):
423 """Updates the commit message.
424
425 Subclass needs to dcommit or push.
426 """
427 self._check_call_git(['commit', '--amend', '-m', commit_message])
428 return self._check_output_git(['rev-parse', 'HEAD']).strip()
429
430 def _check_call_git(self, args, **kwargs):
431 kwargs.setdefault('cwd', self.project_path)
432 kwargs.setdefault('stdout', self.VOID)
[email protected]0bcd1d32011-04-26 15:55:49433 return subprocess2.check_call_out(['git'] + args, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31434
435 def _call_git(self, args, **kwargs):
436 """Like check_call but doesn't throw on failure."""
437 kwargs.setdefault('cwd', self.project_path)
438 kwargs.setdefault('stdout', self.VOID)
439 return subprocess2.call(['git'] + args, **kwargs)
440
441 def _check_output_git(self, args, **kwargs):
442 kwargs.setdefault('cwd', self.project_path)
443 return subprocess2.check_output(['git'] + args, **kwargs)
444
445 def _branches(self):
446 """Returns the list of branches and the active one."""
447 out = self._check_output_git(['branch']).splitlines(False)
448 branches = [l[2:] for l in out]
449 active = None
450 for l in out:
451 if l.startswith('*'):
452 active = l[2:]
453 break
454 return branches, active
455
456
457class GitSvnCheckoutBase(GitCheckoutBase, SvnMixIn):
458 """Base class for git-svn checkout. Not to be used as-is."""
459 def __init__(self,
460 root_dir, project_name, remote_branch,
461 commit_user, commit_pwd,
462 svn_url, trunk):
463 """trunk is optional."""
464 super(GitSvnCheckoutBase, self).__init__(
465 root_dir, project_name + '.git', remote_branch)
466 self.commit_user = commit_user
467 self.commit_pwd = commit_pwd
468 # svn_url in this case is the root of the svn repository.
469 self.svn_url = svn_url
470 self.trunk = trunk
471 assert bool(self.commit_user) >= bool(self.commit_pwd)
472 assert self.svn_url
473 assert self.trunk
474 self._cache_svn_auth()
475
476 def prepare(self):
477 """Resets the git repository in a clean state."""
478 self._check_call_git(['reset', '--hard', '--quiet'])
479 branches, active = self._branches()
480 if active != 'master':
481 if not 'master' in branches:
482 self._check_call_git(
483 ['checkout', '--quiet', '-b', 'master',
484 '%s/%s' % (self.remote, self.remote_branch)])
485 else:
486 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
487 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it.
488 self._check_call_git_svn(['fetch', '--quiet', '--quiet'])
489 self._check_call_git(
490 ['rebase', '--quiet', '--quiet',
491 '%s/%s' % (self.remote, self.remote_branch)])
492 if self.working_branch in branches:
493 self._call_git(['branch', '-D', self.working_branch])
494 return int(self._git_svn_info('revision'))
495
496 def _git_svn_info(self, key):
497 """Calls git svn info. This doesn't support nor need --config-dir."""
498 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key)
499
500 def commit(self, commit_message, user):
501 """Commits a patch."""
502 logging.info('Committing patch for %s' % user)
503 # Fix the commit message and author. It returns the git hash, which we
504 # ignore unless it's None.
505 if not super(GitSvnCheckoutBase, self).commit(commit_message, user):
506 return None
507 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and
508 # doesn't support --with-revprop.
509 # Either learn perl and upstream or suck it.
510 kwargs = {}
511 if self.commit_pwd:
512 kwargs['stdin'] = self.commit_pwd + '\n'
[email protected]8a1396c2011-04-22 00:14:24513 kwargs['stderr'] = subprocess2.STDOUT
[email protected]dfaecd22011-04-21 00:33:31514 self._check_call_git_svn(
515 ['dcommit', '--rmdir', '--find-copies-harder',
516 '--username', self.commit_user],
517 **kwargs)
518 revision = int(self._git_svn_info('revision'))
519 return revision
520
521 def _cache_svn_auth(self):
522 """Caches the svn credentials. It is necessary since git-svn doesn't prompt
523 for it."""
524 if not self.commit_user or not self.commit_pwd:
525 return
526 # Use capture to lower noise in logs.
527 self._check_output_svn(['ls', self.svn_url], cwd=None)
528
529 def _check_call_git_svn(self, args, **kwargs):
530 """Handles svn authentication while calling git svn."""
531 args = ['svn'] + args
532 if not self.svn_config.default:
533 args.extend(['--config-dir', self.svn_config.svn_config_dir])
534 return self._check_call_git(args, **kwargs)
535
536 def _get_revision(self):
537 revision = int(self._git_svn_info('revision'))
538 if revision != self._last_seen_revision:
539 logging.info('Updated at revision %d' % revision)
540 self._last_seen_revision = revision
541 return revision
542
543
544class GitSvnPremadeCheckout(GitSvnCheckoutBase):
545 """Manages a git-svn clone made out from an initial git-svn seed.
546
547 This class is very similar to GitSvnCheckout but is faster to bootstrap
548 because it starts right off with an existing git-svn clone.
549 """
550 def __init__(self,
551 root_dir, project_name, remote_branch,
552 commit_user, commit_pwd,
553 svn_url, trunk, git_url):
554 super(GitSvnPremadeCheckout, self).__init__(
555 root_dir, project_name, remote_branch,
556 commit_user, commit_pwd,
557 svn_url, trunk)
558 self.git_url = git_url
559 assert self.git_url
560
561 def prepare(self):
562 """Creates the initial checkout for the repo."""
563 if not os.path.isdir(self.project_path):
564 logging.info('Checking out %s in %s' %
565 (self.project_name, self.project_path))
566 assert self.remote == 'origin'
567 # self.project_path doesn't exist yet.
568 self._check_call_git(
[email protected]8a1396c2011-04-22 00:14:24569 ['clone', self.git_url, self.project_name, '--quiet'],
570 cwd=self.root_dir,
571 stderr=subprocess2.STDOUT)
[email protected]dfaecd22011-04-21 00:33:31572 try:
573 configured_svn_url = self._check_output_git(
574 ['config', 'svn-remote.svn.url']).strip()
575 except subprocess.CalledProcessError:
576 configured_svn_url = ''
577
578 if configured_svn_url.strip() != self.svn_url:
579 self._check_call_git_svn(
580 ['init',
581 '--prefix', self.remote + '/',
582 '-T', self.trunk,
583 self.svn_url])
584 self._check_call_git_svn(['fetch'])
585 super(GitSvnPremadeCheckout, self).prepare()
586 return self._get_revision()
587
588
589class GitSvnCheckout(GitSvnCheckoutBase):
590 """Manages a git-svn clone.
591
592 Using git-svn hides some of the complexity of using a svn checkout.
593 """
594 def __init__(self,
595 root_dir, project_name,
596 commit_user, commit_pwd,
597 svn_url, trunk):
598 super(GitSvnCheckout, self).__init__(
599 root_dir, project_name, 'trunk',
600 commit_user, commit_pwd,
601 svn_url, trunk)
602
603 def prepare(self):
604 """Creates the initial checkout for the repo."""
605 if not os.path.isdir(self.project_path):
606 logging.info('Checking out %s in %s' %
607 (self.project_name, self.project_path))
608 # TODO: Create a shallow clone.
609 # self.project_path doesn't exist yet.
610 self._check_call_git_svn(
611 ['clone',
612 '--prefix', self.remote + '/',
613 '-T', self.trunk,
[email protected]8a1396c2011-04-22 00:14:24614 self.svn_url, self.project_path,
615 '--quiet'],
616 cwd=self.root_dir,
617 stderr=subprocess2.STDOUT)
[email protected]dfaecd22011-04-21 00:33:31618 super(GitSvnCheckout, self).prepare()
619 return self._get_revision()
620
621
622class ReadOnlyCheckout(object):
623 """Converts a checkout into a read-only one."""
624 def __init__(self, checkout):
625 self.checkout = checkout
626
627 def prepare(self):
628 return self.checkout.prepare()
629
630 def get_settings(self, key):
631 return self.checkout.get_settings(key)
632
[email protected]8a1396c2011-04-22 00:14:24633 def apply_patch(self, patches, post_processor=None):
634 return self.checkout.apply_patch(patches, post_processor)
[email protected]dfaecd22011-04-21 00:33:31635
636 def commit(self, message, user): # pylint: disable=R0201
637 logging.info('Would have committed for %s with message: %s' % (
638 user, message))
639 return 'FAKE'
640
641 @property
642 def project_name(self):
643 return self.checkout.project_name
644
645 @property
646 def project_path(self):
647 return self.checkout.project_path