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