blob: e952b193c1889b930b06e0f5304894615ed37f9e [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
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000298 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000299 handle, commit_filename = tempfile.mkstemp(text=True)
300 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000301 # Shouldn't assume default encoding is UTF-8. But really, if you are using
302 # anything else, you are living in another world.
303 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000304 os.close(handle)
305 # When committing, svn won't update the Revision metadata of the checkout,
306 # so if svn commit returns "Committed revision 3.", svn info will still
307 # return "Revision: 2". Since running svn update right after svn commit
308 # creates a race condition with other committers, this code _must_ parse
309 # the output of svn commit and use a regexp to grab the revision number.
310 # Note that "Committed revision N." is localized but subprocess2 forces
311 # LANGUAGE=en.
312 args = ['commit', '--file', commit_filename]
313 # realauthor is parsed by a server-side hook.
314 if user and user != self.commit_user:
315 args.extend(['--with-revprop', 'realauthor=%s' % user])
316 out = self._check_output_svn(args)
317 finally:
318 os.remove(commit_filename)
319 lines = filter(None, out.splitlines())
320 match = re.match(r'^Committed revision (\d+).$', lines[-1])
321 if not match:
322 raise PatchApplicationFailed(
323 None,
324 'Couldn\'t make sense out of svn commit message:\n' + out)
325 return int(match.group(1))
326
327 def _revert(self):
328 """Reverts local modifications or checks out if the directory is not
329 present. Use depot_tools's functionality to do this.
330 """
331 flags = ['--ignore-externals']
332 if not os.path.isdir(self.project_path):
333 logging.info(
334 'Directory %s is not present, checking it out.' % self.project_path)
335 self._check_call_svn(
336 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
337 else:
338 scm.SVN.Revert(self.project_path)
339 # Revive files that were deleted in scm.SVN.Revert().
340 self._check_call_svn(['update', '--force'] + flags)
341
342 out = self._check_output_svn(['info', '.'])
343 return int(self._parse_svn_info(out, 'revision'))
344
345
346class GitCheckoutBase(CheckoutBase):
347 """Base class for git checkout. Not to be used as-is."""
348 def __init__(self, root_dir, project_name, remote_branch):
349 super(GitCheckoutBase, self).__init__(root_dir, project_name)
350 # There is no reason to not hardcode it.
351 self.remote = 'origin'
352 self.remote_branch = remote_branch
353 self.working_branch = 'working_branch'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000354
355 def prepare(self):
356 """Resets the git repository in a clean state.
357
358 Checks it out if not present and deletes the working branch.
359 """
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000360 assert self.remote_branch
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000361 assert os.path.isdir(self.project_path)
362 self._check_call_git(['reset', '--hard', '--quiet'])
363 branches, active = self._branches()
364 if active != 'master':
365 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
366 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
367 if self.working_branch in branches:
368 self._call_git(['branch', '-D', self.working_branch])
369
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000370 def apply_patch(self, patches, post_processor=None):
371 """Applies a patch on 'working_branch' and switch to it.
372
373 Also commits the changes on the local branch.
374
375 Ignores svn properties and raise an exception on unexpected ones.
376 """
377 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000378 # It this throws, the checkout is corrupted. Maybe worth deleting it and
379 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000380 if self.remote_branch:
381 self._check_call_git(
382 ['checkout', '-b', self.working_branch,
383 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000384 for p in patches:
385 try:
386 stdout = ''
387 if p.is_delete:
388 stdout += self._check_output_git(['rm', p.filename])
389 else:
390 dirname = os.path.dirname(p.filename)
391 full_dir = os.path.join(self.project_path, dirname)
392 if dirname and not os.path.isdir(full_dir):
393 os.makedirs(full_dir)
394 if p.is_binary:
395 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
396 f.write(p.get())
397 stdout += self._check_output_git(['add', p.filename])
398 else:
399 stdout += self._check_output_git(
400 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get())
401 for prop in p.svn_properties:
402 # Ignore some known auto-props flags through .subversion/config,
403 # bails out on the other ones.
404 # TODO(maruel): Read ~/.subversion/config and detect the rules that
405 # applies here to figure out if the property will be correctly
406 # handled.
407 if not prop[0] in ('svn:eol-style', 'svn:executable'):
408 raise patch.UnsupportedPatchFormat(
409 p.filename,
410 'Cannot apply svn property %s to file %s.' % (
411 prop[0], p.filename))
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000412 for post in post_processor:
413 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000414 except OSError, e:
415 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
416 except subprocess.CalledProcessError, e:
417 raise PatchApplicationFailed(
418 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
419 # Once all the patches are processed and added to the index, commit the
420 # index.
421 self._check_call_git(['commit', '-m', 'Committed patch'])
422 # TODO(maruel): Weirdly enough they don't match, need to investigate.
423 #found_files = self._check_output_git(
424 # ['diff', 'master', '--name-only']).splitlines(False)
425 #assert sorted(patches.filenames) == sorted(found_files), (
426 # sorted(out), sorted(found_files))
427
428 def commit(self, commit_message, user):
429 """Updates the commit message.
430
431 Subclass needs to dcommit or push.
432 """
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000433 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000434 self._check_call_git(['commit', '--amend', '-m', commit_message])
435 return self._check_output_git(['rev-parse', 'HEAD']).strip()
436
437 def _check_call_git(self, args, **kwargs):
438 kwargs.setdefault('cwd', self.project_path)
439 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000440 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000441
442 def _call_git(self, args, **kwargs):
443 """Like check_call but doesn't throw on failure."""
444 kwargs.setdefault('cwd', self.project_path)
445 kwargs.setdefault('stdout', self.VOID)
446 return subprocess2.call(['git'] + args, **kwargs)
447
448 def _check_output_git(self, args, **kwargs):
449 kwargs.setdefault('cwd', self.project_path)
450 return subprocess2.check_output(['git'] + args, **kwargs)
451
452 def _branches(self):
453 """Returns the list of branches and the active one."""
454 out = self._check_output_git(['branch']).splitlines(False)
455 branches = [l[2:] for l in out]
456 active = None
457 for l in out:
458 if l.startswith('*'):
459 active = l[2:]
460 break
461 return branches, active
462
463
464class GitSvnCheckoutBase(GitCheckoutBase, SvnMixIn):
465 """Base class for git-svn checkout. Not to be used as-is."""
466 def __init__(self,
467 root_dir, project_name, remote_branch,
468 commit_user, commit_pwd,
469 svn_url, trunk):
470 """trunk is optional."""
471 super(GitSvnCheckoutBase, self).__init__(
472 root_dir, project_name + '.git', remote_branch)
473 self.commit_user = commit_user
474 self.commit_pwd = commit_pwd
475 # svn_url in this case is the root of the svn repository.
476 self.svn_url = svn_url
477 self.trunk = trunk
478 assert bool(self.commit_user) >= bool(self.commit_pwd)
479 assert self.svn_url
480 assert self.trunk
481 self._cache_svn_auth()
482
483 def prepare(self):
484 """Resets the git repository in a clean state."""
485 self._check_call_git(['reset', '--hard', '--quiet'])
486 branches, active = self._branches()
487 if active != 'master':
488 if not 'master' in branches:
489 self._check_call_git(
490 ['checkout', '--quiet', '-b', 'master',
491 '%s/%s' % (self.remote, self.remote_branch)])
492 else:
493 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
494 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it.
495 self._check_call_git_svn(['fetch', '--quiet', '--quiet'])
496 self._check_call_git(
497 ['rebase', '--quiet', '--quiet',
498 '%s/%s' % (self.remote, self.remote_branch)])
499 if self.working_branch in branches:
500 self._call_git(['branch', '-D', self.working_branch])
501 return int(self._git_svn_info('revision'))
502
503 def _git_svn_info(self, key):
504 """Calls git svn info. This doesn't support nor need --config-dir."""
505 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key)
506
507 def commit(self, commit_message, user):
508 """Commits a patch."""
509 logging.info('Committing patch for %s' % user)
510 # Fix the commit message and author. It returns the git hash, which we
511 # ignore unless it's None.
512 if not super(GitSvnCheckoutBase, self).commit(commit_message, user):
513 return None
514 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and
515 # doesn't support --with-revprop.
516 # Either learn perl and upstream or suck it.
517 kwargs = {}
518 if self.commit_pwd:
519 kwargs['stdin'] = self.commit_pwd + '\n'
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000520 kwargs['stderr'] = subprocess2.STDOUT
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000521 self._check_call_git_svn(
522 ['dcommit', '--rmdir', '--find-copies-harder',
523 '--username', self.commit_user],
524 **kwargs)
525 revision = int(self._git_svn_info('revision'))
526 return revision
527
528 def _cache_svn_auth(self):
529 """Caches the svn credentials. It is necessary since git-svn doesn't prompt
530 for it."""
531 if not self.commit_user or not self.commit_pwd:
532 return
533 # Use capture to lower noise in logs.
534 self._check_output_svn(['ls', self.svn_url], cwd=None)
535
536 def _check_call_git_svn(self, args, **kwargs):
537 """Handles svn authentication while calling git svn."""
538 args = ['svn'] + args
539 if not self.svn_config.default:
540 args.extend(['--config-dir', self.svn_config.svn_config_dir])
541 return self._check_call_git(args, **kwargs)
542
543 def _get_revision(self):
544 revision = int(self._git_svn_info('revision'))
545 if revision != self._last_seen_revision:
546 logging.info('Updated at revision %d' % revision)
547 self._last_seen_revision = revision
548 return revision
549
550
551class GitSvnPremadeCheckout(GitSvnCheckoutBase):
552 """Manages a git-svn clone made out from an initial git-svn seed.
553
554 This class is very similar to GitSvnCheckout but is faster to bootstrap
555 because it starts right off with an existing git-svn clone.
556 """
557 def __init__(self,
558 root_dir, project_name, remote_branch,
559 commit_user, commit_pwd,
560 svn_url, trunk, git_url):
561 super(GitSvnPremadeCheckout, self).__init__(
562 root_dir, project_name, remote_branch,
563 commit_user, commit_pwd,
564 svn_url, trunk)
565 self.git_url = git_url
566 assert self.git_url
567
568 def prepare(self):
569 """Creates the initial checkout for the repo."""
570 if not os.path.isdir(self.project_path):
571 logging.info('Checking out %s in %s' %
572 (self.project_name, self.project_path))
573 assert self.remote == 'origin'
574 # self.project_path doesn't exist yet.
575 self._check_call_git(
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000576 ['clone', self.git_url, self.project_name, '--quiet'],
577 cwd=self.root_dir,
578 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000579 try:
580 configured_svn_url = self._check_output_git(
581 ['config', 'svn-remote.svn.url']).strip()
582 except subprocess.CalledProcessError:
583 configured_svn_url = ''
584
585 if configured_svn_url.strip() != self.svn_url:
586 self._check_call_git_svn(
587 ['init',
588 '--prefix', self.remote + '/',
589 '-T', self.trunk,
590 self.svn_url])
591 self._check_call_git_svn(['fetch'])
592 super(GitSvnPremadeCheckout, self).prepare()
593 return self._get_revision()
594
595
596class GitSvnCheckout(GitSvnCheckoutBase):
597 """Manages a git-svn clone.
598
599 Using git-svn hides some of the complexity of using a svn checkout.
600 """
601 def __init__(self,
602 root_dir, project_name,
603 commit_user, commit_pwd,
604 svn_url, trunk):
605 super(GitSvnCheckout, self).__init__(
606 root_dir, project_name, 'trunk',
607 commit_user, commit_pwd,
608 svn_url, trunk)
609
610 def prepare(self):
611 """Creates the initial checkout for the repo."""
612 if not os.path.isdir(self.project_path):
613 logging.info('Checking out %s in %s' %
614 (self.project_name, self.project_path))
615 # TODO: Create a shallow clone.
616 # self.project_path doesn't exist yet.
617 self._check_call_git_svn(
618 ['clone',
619 '--prefix', self.remote + '/',
620 '-T', self.trunk,
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000621 self.svn_url, self.project_path,
622 '--quiet'],
623 cwd=self.root_dir,
624 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000625 super(GitSvnCheckout, self).prepare()
626 return self._get_revision()
627
628
629class ReadOnlyCheckout(object):
630 """Converts a checkout into a read-only one."""
631 def __init__(self, checkout):
632 self.checkout = checkout
633
634 def prepare(self):
635 return self.checkout.prepare()
636
637 def get_settings(self, key):
638 return self.checkout.get_settings(key)
639
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000640 def apply_patch(self, patches, post_processor=None):
641 return self.checkout.apply_patch(patches, post_processor)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000642
643 def commit(self, message, user): # pylint: disable=R0201
644 logging.info('Would have committed for %s with message: %s' % (
645 user, message))
646 return 'FAKE'
647
648 @property
649 def project_name(self):
650 return self.checkout.project_name
651
652 @property
653 def project_path(self):
654 return self.checkout.project_path